React 製アプリケーションのビルドシステムを webpack から Vite に移行して爆速な開発体験を手に入れよう
wakamsha
Vite (ヴィート)とは
Vue.js の作者である Evan You 氏が中心となって開発されているビルドツールです。
ES Modules 形式のままブラウザからインポートする Dev サーバを搭載し、ソースコードをバンドルすることなく高速で動作させるのが特徴です。もちろん npm パッケージもブラウザから読み込み可能な ES Modules 形式に変換します。プロダクションビルド時は Rollup を使ってバンドルします。
Vue.js だけでなく React、Preact、Svelte のビルドもサポートしており、GitHub トレンドの上位にも頻繁に登場していることからその注目度合いがうかがえます。
本エントリでは、 React 製アプリケーションのビルドシステムを webpack から Vite に移行するサンプルをご紹介します。
ソースコードはこちら。
まずはデモ動画を
対象アプリケーションの移行前の技術スタック
- TypeScript
- React + React Router
- Emotion
- webpack v5
非常にシンプルな SPA です。極力 JavaScript ( TypeScript ) の世界だけで完結するよう CSS は CSS-in-JS を使っています。本サンプルでは @emotion/css を使用していますが、 styled-components に読み替えていただいても大丈夫です。
また、本サンプルの技術スタックに Babel は含めていません。Microsoft の IE11 のサポート終了に関する情報が公開されるにつれてアプリケーション開発界隈でも IE11 のサポートを終了する動きが目立ってきています。よって本エントリも IE11 終了後を見据えたうえで話を進めます。
webpack でのビルド設定
比較のため、まずは webpack ( v5 ) によるビルド設定を確認しておきます。使用する npm パッケージは下記のとおり。
- ts-loader
- webpack
- webpack-cli
- webpack-dev-server
// @ts-check
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (_env, { mode = 'development' }) => {
const develop = mode === 'development';
return {
mode,
entry: {
app: ['./src/index.tsx'],
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
assetModuleFilename: 'assets/[name][ext]',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
modules: [
path.resolve(__dirname, './src'),
path.resolve(__dirname, './node_modules'),
],
},
module: {
rules: [
// 1.
{
test: /\.tsx?$/,
exclude: /node_modules/,
// 1-a.
use: [{ loader: 'ts-loader', options: { transpileOnly: develop } }],
},
// 2.
{
test: /\.(ico|svg|jpe?g|png|webp|woff)$/,
type: 'asset/resource',
},
],
},
optimization: {
// 3.
splitChunks: {
name: 'vendor.bundle',
// Dynamic import に関連しない node_modules から呼び出すライブラリをひとまとめにする
chunks: ({ name }) => name === 'app',
},
minimizer: [
// 4.
new TerserPlugin({
extractComments: {
condition: /^\**!|@preserve|@license|@cc_on/i,
filename: 'licenses.txt',
},
terserOptions: {
output: {
// 対象とするコメントの pattern
comments: /^\**!|@preserve|@license|@cc_on/,
},
},
}),
],
},
// 5.
devtool: develop ? 'eval-cheap-module-source-map' : 'source-map',
// 6.
...(develop
? {
devServer: {
port: 3000,
open: true,
hot: true,
publicPath: '/',
contentBase: path.join(__dirname, 'dist/'),
historyApiFallback: {
index: '/',
},
},
}
: {}),
};
};
- ts-loader で TypeScript をトランスパイルする
- 1-a. 型チェックはせずトランスパイルのみを行う
- 1-b. 型チェックは
tsc --noEmit
コマンドを別途実行することで並列で行う
- 画像などアセットファイルを読み込む(いわゆる file-loader)
- Split Chunk(アプリケーションコードとライブラリコードを別々にバンドルする)
- minify 時にライブラリのライセンス用コメント部分を消さないようにする
- TypeScript の source maps を生成する
- 開発時は Dev server を起動する
Babel や CSS Modules も使わない至ってシンプルな構成です。にも関わらず結構な行数のコード量となってしまいました。プロジェクトによってはここから更に多くの設定を追加することになり、開発者は少しでも保守性を維持するべく関数を小分けにして切り出すなどの工夫を凝らすこととなるでしょう。それ自体は決して悪いことではありませんが、秘伝のタレ化してしまう印象は避けられません。せめて定型的な処理はビルドツールの方で良しなにお世話しつつ隠蔽していただきたいものです。
Vite で実現できること
webpack で行ったものの多くは Vite で再現可能です。
webpack | Vite | |
---|---|---|
TypeScript トランスパイル | ✅ ※ 別途 ts-loader が必要 |
✅ ※ ただし型チェックは無し |
ES Modules 向けビルド | ❌ | ✅ |
config ファイルを TypeSciript で書く | ✅ ※ 別途 ts-node が必要1)Configuration Languages - webpack |
✅ |
Split Chunk | ✅ ※ 分割する処理を自前で定義する必要がある |
✅ ※ 設定無しで npm パッケージとアプリケーションコードとで分割してくれる |
Minify | ✅ ※ npm パッケージのライセンスコメントを残す処理を別途定義する必要がある |
✅ ※ 設定無しでライセンスコメントを残してくれる |
アセットファイルのインポート | ✅ ※ v4.x までは file-loader が必要 |
✅ |
dev server | ✅ ※ 別途 webpack-dev-server が必要 |
✅ |
tsconfig#compilerOptions#jsx の react-jsx サポート |
✅ | ✅ ※ config 設定で(擬似的に)サポート |
HMR | ✅ | ✅ |
source maps | ✅ | ✅ |
グローバル変数注入(Define) | ✅ | ✅ |
yarn workspace ( monorepo ) | ✅ | ✅ |
その他エコシステム | 膨大なサードパーティ製プラグイン | Rollup 用プラグインが流用可能 |
この他にも多くの機能を有しており、すでに次世代のビルドツールとして充分に現実的な候補と言えるでしょう。
そしてなにより TypeScript トランスパイルの圧倒的な速さは魅力的です。Vite は esbuild を使って TypeScript をビルドするため非常に快適な開発体験が得られます。ただし型チェックは行えないため、 tsc --noEmit
コマンドを並列実行するなりして担保します。
Vite の config ファイル自体を .ts
として書けるのも大きいです。これにより config ファイル作成時も型チェックや入力補完等の恩恵を充分に受けられ、更に ES Modules にも対応してるので import
句の使用も可能です。
新規参入したシステムの弱点としてエコシステムの未発達が挙げられますが、Vite は Rollup 用プラグインと互換性を持つことでこの弱点を克服しています2)ただし全てが使えるわけではないとのこと。
ビルドシステムを Vite に置き換える
ようやく本題です。使用するライブラリは下記の通り。
コアライブラリである vite
と React をサポートするプラグインの2つを使います。依存ライブラリが少なく済むのは長期の運用保守を考えると嬉しいですね。
config ファイルを作成する
といっても記述量は非常に少なく簡単です。というのもこれまで webpack.config
に自前で記述していたものの多くがデフォルトで設定されているため、自前で書くものがあまりないのです。
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [react()],
esbuild: {
jsxInject: `import React from 'react';`,
},
build: {
sourcemap: true,
},
});
なんとたったのこれだけです。この僅かなコードだけで先程の webpack.config
と同等の開発基盤が構築されます。
React のビルドとホットリロード
@vitejs/plugin-react
は、React プロジェクト用のオールインワン・プラグインです。これ一つ導入するだけで React 製アプリのビルドとホットリロードが実現できます。
export default defineConfig({
plugins: [react()],
...
});
新しい JSX 変換に対応する
React 17 + TypeScript 4.1 より新しい JSX の変換方式が加わったことにより、tsconfig の compilerOptions
に下記を追記することで import React from 'react';
をソースコードに記述する必要がなくなりました。
{
"compilerOptions": {
...
"jsx": "react-jsx"
}
}
しかしこれはビルドツールに tsc, webpack, Babel を用いた場合の話であって、2021年6月現在 esbuild はこの変換方式をサポートしていません3)Issue は挙がっているものの、コアメンバーがサポートに消極的な様子。
Support jsx automatic runtime · Issue #334 · evanw/esbuild。そのためオプションを駆使してこの変換方式を擬似的にサポートさせます。
esbuild
はその名の通り esbuild トランスパイルオプションを拡張できるプロパティです。Vite は esbuild を使って TypeScript をトランスパイルしますが、jsxInject
を使用することで全 tsx ファイルに import
文を注入します。
export default defineConfig({
...
esbuild: {
jsxInject: `import React from 'react';`,
},
});
これで Vite ( esbuild ) でも同等の結果を得られます。
エントリポイントとなる HTML ファイルを用意する
webpack ではアプリケーションへのエントリポイント(パス)を webpack.config
の entry
プロパティで指定しました。Vite では HTML ファイル内に直接エントリポイントを記述します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
デフォルトでは vite.config
は同じディレクトリ4)process.cwd()
にある index.html
を参照し、そこに記述されている <script type="module">
要素の src
パスをエントリポイントと見なして処理します。今回は /src
ディレクトリ直下の index.tsx
がエントリポイントとなりますので、 src="/src/index.tsx"
と記述します。
index.html
を vite.config
とは異なるディレクトリに配置したい場合は、 root
オプションを上書きします。
export default defineConfig({
...
root: '/path/to/html',
});
Dev サーバを起動する
下記のコマンドを実行すると http://localhost:3000
が起動します。
yarn vite
コマンド実行からわずか1~2秒でアプリケーションが起動してるのが分かります。
Port の設定
デフォルトの port
番号は 3000
ですが、これもオプションから上書きできます。
export default defineConfig({
...
server: {
port: 3333,
},
});
webpack でもビルドしてみる
Vite と比較してアプリケーションが立ち上がるまで 5~6秒は要してるのが分かります。デモアプリが小規模なのでそこまで大きな差は出てませんが、それでも大規模化するにつれてこの差は大きなものとなります。
Production Build
下記のコマンドを実行するとプロダクションビルドが実行されます。
yarn vite build
デフォルトの書き出し先ディレクトリは ./dist
ですが、これもオプションから上書きできます。
export default defineConfig({
...
build: {
outDir: '/path/to/outputDirectory',
},
});
締め: 実戦投入するうえでも申し分ないクオリティ
駆け足になりましたが、webpack と比較しつつ Vite の導入について解説しました。エコシステムの成熟度合いを鑑みれば webpack に一日の長があることは否定のしようがありませんが、新規の案件であれば充分に実戦投入に足るクオリティを備えていると言って良いでしょう。
脚注
↑1 | Configuration Languages - webpack |
---|---|
↑2 | ただし全てが使えるわけではないとのこと |
↑3 | Issue は挙がっているものの、コアメンバーがサポートに消極的な様子。 Support jsx automatic runtime · Issue #334 · evanw/esbuild |
↑4 | process.cwd() |