はじめに
Webpackよりビルドが速いと言われているフロントエンドビルドツール「Vite」のバージョン3が先日正式版が公開されました。これを機にViteについて調べて見たので紹介します。
なぜViteは早いのか
Native ESMの活用
Viteで一番わかりやすいことは、開発中のビルドが早いことだと思います。これは、Webpackのように、実装されたimportやexportを元に1つのJSファイルにバンドルするのではなく、ブラウザによって各モジュールをimportさせるようにしているため、バンドルの手間が省けているからです。
このような、ブラウザによるES Modulesの仕組みをNative ESMと呼びます。ES2015で定義された仕様で、モダンブラウザであれば対応しています。
実際に、プロジェクト生成直後にビルドされたコードを見てみます。
まず、以下がindex.htmlです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!DOCTYPE html> <html lang="en"> <head> <script type="module"> import RefreshRuntime from "/@react-refresh" RefreshRuntime.injectIntoGlobalHook(window) window.$RefreshReg$ = () => {} window.$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true </script> <script type="module" src="/@vite/client"></script> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React + TS</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html> |
21行目で
main.tsx
が読み込まれているので、main.tsxを見てみます。ちなみに、
type="module"
は、HTMLにModuleを埋め込む際、
src
が示すパスがモジュールであることを明示するために必要なものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var _jsxFileName = "/home/kaiser/study/study-vite/study-vite-react/src/main.tsx"; import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=7bc51448"; const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; import __vite__cjsImport1_reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v=39c8951a"; const ReactDOM = __vite__cjsImport1_reactDom_client.__esModule ? __vite__cjsImport1_reactDom_client.default : __vite__cjsImport1_reactDom_client; import App from "/src/App.tsx"; import "/src/index.css"; import { jsxDEV as _jsxDEV } from "/@id/__x00__react/jsx-dev-runtime"; ReactDOM.createRoot(document.getElementById("root")).render(/* @__PURE__ */ _jsxDEV(React.StrictMode, { children: /* @__PURE__ */ _jsxDEV(App, {}, void 0, false, { fileName: _jsxFileName, lineNumber: 8, columnNumber: 5 }, this) }, void 0, false, { fileName: _jsxFileName, lineNumber: 7, columnNumber: 3 }, this)); |
拡張子はtsxとなっていますが、中身はJSにコンパイルされていました。このソースを見て分かるとおり、import文によって、各モジュールがインポートされています。
それでは、プロダクションビルドの場合はどうでしょうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React + TS</title> <script type="module" crossorigin src="/assets/index.27ee3c8b.js"></script> <link rel="stylesheet" href="/assets/index.3fce1f81.css"> </head> <body> <div id="root"></div> </body> </html> |
先程のように、body内のscriptタグではなく、head内のscriptタグによって、
index.27ee3c8b.js
が埋め込まれています。このJSファイルは先ほどとは異なり、従来どおりバンドルされたJSファイルです。
この理由は、importによる依存が多い場合、ブラウザはそれらを1つひとつHTTPリクエストするため、大量のリクエストが発生してしまい、パフォーマンスの問題があるためだそうです。
esbuildによる事前バンドル
先ほど、importによる依存が多い場合、大量のHTTPリクエストによるパフォーマンスの問題があることに触れました。これはローカルの開発においても起こりえます。例えば、複雑な依存関係を持つ外部ライブラリをimportする場合などです。
この問題に対応するため、Viteではesbuildによる事前バンドルが行われます。esbuildはyarn.lockにあるパッケージ単位でキャッシュされるため、一度事前バンドルがされれば、2回目以降は高速です。しかも、esbuild自体はGo言語で開発されたバイナリであるため、Node.jsベースのバンドラーよりも高速です。
yarn.lockにあるバージョンが更新されたタイミングで、キャッシュが更新される仕組みになっています。
また、ESM非対応のモジュールをESM対応させる目的もあります。例えば、Reactのindex.jsを見てみると、以下のようにCommon JSになっていることが分かります。
1 2 3 4 5 6 7 | 'use strict'; if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.min.js'); } else { module.exports = require('./cjs/react.development.js'); } |
ですが、先ほどのDevビルドをのソースでは、Reactがimport文でインポートされています。
1 | import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=7bc51448"; const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; |
このように、CommonJSなどのESM非対応モジュールも、esbuildによってESMに変換され、ブラウザから直接importできるようになっています。
Viteのセットアップ
ここからはViteを実際にセットアップする方法を紹介します。
Reactテンプレートでのセットアップ
yarn create vite
コマンドを使うと、対話形式でプロジェクト名・テンプレートをしてViteプロジェクトを作成することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ yarn create vite yarn create v1.22.19 [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages... success Installed "create-vite@3.0.0" with binaries: - create-vite - cva ✔ Project name: … study-vite-react ✔ Select a framework: › react ✔ Select a variant: › react-ts Scaffolding project in /study-vite/study-vite-react... Done. Now run: cd study-vite-react yarn yarn dev Done in 57.74s. |
Vite 2の時のテンプレートはReact 17だったのですが、現在ではReact 18に対応しています。
viteのyarnコマンド
デフォルトではpackage.jsonで以下のように定義されています。
1 2 3 4 5 | "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, |
-
yarn dev
開発サーバを起動します。デフォルトではlocalhost:5173で起動します。 -
yarn build
プロダクション用のビルドをdist
ディレクトリに生成します。 -
yarn preview
dist
ディレクトリ内のビルドをローカルでプレビューするためのサーバを起動します。
設定
Viteの設定の中で、今回調べた設定を2つ紹介します。
プロキシの設定
Viteにはnode-http-proxyが組み込まれているので、開発サーバのプロキシを設定できます。Webpackにも似たような機能はありますが、複数のターゲットを設定することができたり、ルールを柔軟に設定することが出来ます。
今回は、Github APIをリクエストするための設定を追加してみます。
1 2 3 4 5 6 7 8 9 10 11 12 | export default defineConfig({ plugins: [react()], server: { proxy: { "/api": { target: "https://api.github.com", changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ""), }, }, }, }); |
この設定を追加すると
http://localhost:5173/api/repos/octocat/hello-world/issues
といったリクエストが出来るようになります。クロスオリジンとなる場合は
changeOrigin: true
としておきます。また、Githubの場合は、パスに
/api
は含まれないため、
rewrite
で除去します。
環境変数
CRAでは
REACT_APP_
というプレフィックスを付けると、アプリケーション内で環境変数を使えましたが、Viteでは
VITE_
というプレフィックスを付けます。また、ソースコード内で使用するときは
import.meta.env.VITE_HOGE
というようにアクセスします。
TypeScriptでの補完が必要な場合は、次のように型を拡張することで対応できます。
1 2 3 4 5 6 7 8 9 | /// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_MY_VALUE: string; } interface ImportMeta { readonly env: ImportMetaEnv; } |
このファイルを
src/env.d.ts
に配置すれば、TypeScriptの補完を効かせることが出来ます。
静的アセット
Webpackの
file-loader
と似たような機能で、画像ファイルなどの静的アセットを読み込むことが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 | import reactLogo from "./assets/react.svg"; // 画像ファイル function App() { return ( <div className="App"> <img src={reactLogo} className="logo react" alt="React logo" /> </div> ); } export default App; |
import reactLogo from "./assets/react.svg"
のようにimportすると、
reactLogo
にファイルへの相対パスが入るため、imgタグのsrcに入れるだけで画像を表示させることが出来ます。
さいごに
開発ビルドについての速さの仕組みや様々な設定について理解することが出来ました。今後、プロダクションビルドでのビルドのスピードについても調べたいと思います。