👻

[Vite]Vue2の既存Project(Webpack5)でビルドに10分かかってたのをViteにした瞬間、1秒で終わるようになった話

2022/04/17に公開

キャッチーなタイトル

初めまして。小さいベンチャー企業でエンジニアマネージャー兼フロントエンドエンジニアをやっているJoeyです。
今回はタイトルの通り、そこそこ大規模なProjectのビルドツールをViteに変えたら、ビルド速度が信じられないほど高速化し、更に肥大化していたmoduleもがっつり減って管理しやすくなった話をします。

既存のProjectをどうやってWebpackからViteに移行したのかも説明します。

Vite変更前、Webpackの構成

元々TSLintWebpack4など化石化していたProjectだったんですが、いったんESLintとWebpackもバージョンを5にあげました。ここでPrettierも導入して、ソースコードをFormatしたりしました。
そんな状態で、ビルドまで10分かかっていたWebpackの構成がこちらです。
化石プロジェクトで冗長かつよろしくない表記が多々見られますが、ヤバさを伝えるために全コード乗せます。

webpack.config.js

Vue2 / Typescript / SCSS

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = (env) => {
  if (module.hot) module.hot.accept();
  return {
    entry: './src/main.ts',
    cache: {
      type: 'filesystem',
      buildDependencies: {
        config: [__filename],
      },
    },
    module: {
      rules: [
        {
          test: /\.vue$/,
          use: ['vue-loader'],
        },
        {
          test: /\.scss$/,
          use: ['style-loader', 'css-loader', 'sass-loader'],
        },
        {
          loader: 'ts-loader',
          test: /\.ts$/,
          exclude: [/node_modules/],
          options: {
            configFile: 'tsconfig.json',
            appendTsSuffixTo: [/\.vue$/],
          },
        },
        {
          test: /\.(jpe?g|png|gif|svg)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: '[folder]/[name].[ext]',
                outputPath: '../img/',
              },
            },
          ],
        },
      ],
    },
    resolve: {
      extensions: ['.mjs', '.ts', '.js', '.vue'],
      alias: {
        vue$: 'vue/dist/vue.esm.js',
        '@': path.resolve(__dirname, 'src'),
      },
    },
    plugins: [
      new VueLoaderPlugin(),
      new ESLintPlugin({
        extensions: ['.ts', '.js', '.vue'],
        exclude: 'node_modules',
        fix: true,
      }),
      new SpeedMeasurePlugin(),
      new CopyWebpackPlugin(
        [
          {
            from: '.',
            to: '../img/',
          },
        ],
        {
          context: 'src/img/',
          postcss: [
            require('autoprefixer')({
              '`overrideBrowserslist`': ['last 2 versions'],
            }),
          ],
        }
      ),
    ],
    output: {
      library: 'VueApp',
      path: path.resolve(__dirname, 'dst', 'js'),
      publicPath: '/js/',
      filename: 'app.js',
      libraryTarget: 'umd',
    },
    devServer: {
      static: {
        directory: path.resolve(__dirname, 'dst'),
      },
      open: true,
    },
  };
};

ざっくりやっているのはsrc以下の.vue, .scss, .img系のファイルをトランスパイルしたりバンドルしたりしてdst以下に吐き出している感じです。index.htmlでVueAppを呼び出してレンダリングしてます。

やっていることは極シンプルなんですが、異様にWebpackのコード量が多いですね。というかそもそもWebpackってコードが大きくなりがちな気もしています。これをきちんと管理してやれば10分のビルド時間もかかんないんじゃないか……とも思いましたが、管理のしやすさも魅力だったのでこれを機にViteに変えることを決意しました。

まずはProjectにViteをinstallします。さらに、Vue2用にPluginも併せて入れましょう。
npm install vite-plugin-vue2 vite@latest -D
package.jsonのscriptに下記を追加します。

  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },

次にsrc/vite.config.tsを作成します。
サンプルでVite(Vue3 + TS)を使ってみたときに残っていたconfig等があったので、それをベースにして要件にあわせて調整させました。すでにtsconfig.jsonなど定義してあったので直下でcreateしたら変な挙動しそうだったため、もし同じような状況なら一旦別のディレクトリで新規にViteでProjectを作って、そこのconfig系をコピーしてきた方がいいと思います。
create vite@latest my-vue-app --template vue-ts
Vue + TSの場合はこれで作れるやつですね。

vite.config.ts
vite.config.ts
import { defineConfig } from 'vite'
// import vue from '@vitejs/plugin-vue'; // vue3のみ
import { createVuePlugin } from 'vite-plugin-vue2' // vue2の場合はこれを使う
import alias from '@rollup/plugin-alias'
const { resolve } = require('path')

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    createVuePlugin(/* options */),
    alias({
      entries: {
        '@': resolve(__dirname, 'src'),
        '~': resolve(__dirname, 'node_modules'),
      },
    }),
  ],
  resolve: {
    alias: {
      './runtimeConfig': './runtimeConfig.browser',
      vue: require.resolve('vue/dist/vue.js'),
    },
  },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
      },
    },
  },
})

いくつか要件に合わせてカスタマイズしました。
ポイントは3つありまして、まず1つがVue2の対応です。

Vue2対応(Vite)

vite.config.tsから抜粋

import { createVuePlugin } from 'vite-plugin-vue2'

  plugins: [
    createVuePlugin(/* options */),

この箇所で対応しています。Vue3の場合はimport vue from '@vitejs/plugin-vue';をPluginに読み込ませれば良いですね。

Vue完全ビルド設定の対応

2つ目は現状使っているVue2が完全ビルドだったため、aliasに定義してあげる必要があったこと。また、IEなんてもう対応させる意味はないのでhtmlからは直接tsを読み込むようにさせたこと。

(あと地味にhtml / img系のディレクトリはdstからRootに置き換えています。Viteはindex.htmlをRootで読むので

一応、index.html
index.html
  <body>
    <div id="app"></div>
	  <script type="module" src="/src/main.ts"></script>
  </body>
  resolve: {
    alias: {
      vue: require.resolve('vue/dist/vue.js'),
    },

こんなふうに完全ビルドの場合、aliasでvue: require.resolve('vue/dist/vue.js'),と定義してあげる必要があります。

aliasの解決(rootを@/で書きたい)

3つ目が地味にデカいポイントですが、Viteではaliasの解決が独特らしく/@みたいに/から始める必要があるらしい。でも今のファイルはすべて@から始めていたので、全てに/を入れるのは一苦労……と思いましたが、どうもViteはRollupのPluginがほぼ全て動くそうです(凄い!)

なのでRollupのaliasプラグインを導入しました。書き方は以下の通り。

import alias from '@rollup/plugin-alias'
const { resolve } = require('path')

  plugins: [
    alias({
      entries: {
        '@': resolve(__dirname, 'src'),
        '~': resolve(__dirname, 'node_modules'),
      },
    }),
  ],

素晴らしい。あとnode直下も定義しておきました。
他にもRollupは色々あるので、便利ですね。

以上、configが定義し終わりました。見比べてもらえればわかりますがSimple2022BEST。
Webpackは無駄にオブジェクトのネストが多くてとんでもなく見づらかったですが、Viteは非常に簡潔で良いですね。TSやSCSSもよしなに解釈してくれるので本当に素晴らしい。

TS周りの設定ファイル(tsconfig.json)

あとはTS周りの設定ですが、Vite(Vue3 + TS)を使ってみたときに残っていたconfig系から引っ張ってきたものがほとんどです。tsconfigはちょっと調整していますが、ほぼテンプレートそのままです。

ts系設定ファイル

tsconfig.node.jsonをコピペして、

tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node"
  },
  "include": ["vite.config.ts"]
}

tsconfig.jsonをコピペして、案件ごとに調整してあげるだけ。

tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "preserve",
    "resolveJsonModule": true,
    "lib": ["esnext", "dom"],
    "strictPropertyInitialization": false,
    "types": ["node"],
    "sourceMap": true,
    "strict": true,
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "esModuleInterop": true,
    "experimentalDecorators": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

そして一通り設定ができたので、動かします。

起動からのバグ取り

起動させてみるとページは真っ白、エラーを吐き出していました。
それらを一つずつ潰していきます。でも、それほど多くなかったです。

amplifyのバグ

だいたいこいつバグってる気がするんですが、とにかく依存関係周りでエラーを吐き出していました。
issueにも多く上がっています。
Error: 'request' is not exported by __vite-browser-external, imported by node_modules/.pnpm/@aws-sdk/credential-provider-imds@3.6.1/node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js

この通りにしたら直りました。

index.htmlに

    <script>
      if (global === undefined) {
        var global = window;
      }
    </script>

globalにwindowオブジェクトを入れてあげて、vite.config.tsのaliasにランタイムを定義します。

vite.config.ts
resolve: {
 alias: {
  './runtimeConfig': './runtimeConfig.browser',
 },
},

非ESモジュールで作られているライブラリについて

The requested module @modules does not provide an export named
こんなエラーが出ます。あまりに古いライブラリはそもそもexportを持たないので、Viteは読み込むことができないようです。

これに関して、僕はもう捨てました。
幸いなことに2つしかなかったので、これらのライブラリは別のに置き換えるか自前で実装し直すかすべきでしょう。そもそもexportされていない時点でメンテナンスされていないような古いライブラリなので、リプレイスを考えるべきだと思います。少なくとも、僕はそうしちゃいました。

動きました!!!

バグを取り終わり、npm run devしてみるとちゃんとビルドされました!
10分かかってたビルドが1秒です。凄すぎる。これがバンドルしないビルドツールか……

Webpackのようなバンドルツールとの違いの一つで、ViteはTypescriptの型チェックを行いません。ですので上記の状態でも動きますが、あくまでこれは既存ProjectがLinterで制御されており、型エラーがない前提なのでちゃんと動いています。

現状eslintを入れていないですが、入れることも可能です。とはいえエディター側で検知されるので、普通にあの赤い波線がなくなるように書いていれば問題はないですね。心配ならeslintを入れてデプロイ前などに走らせればいいと思います。Viteに組み込む必要はないんじゃないかなと思いました。これも時代ですね。

実際移行してみると、開発体験もDevOpsの管理のしやすさも段違いです。
この記事が移行を検討しているみなさまの一助になると嬉しいです。では!


よかったらTwitterのフォローなどお願いします!仲良くしてください!

Discussion