Skip to content

Vite の所感

最近触ってるコードベースのフロントエンドのコードのバンドルを試験的に Vite に置き換えてみた。(まだ途中だが)
そこで、一通り動くようにしてみて感じたことを書く。マージするかは未定だが、おそらくする予定。
また、全部実装しきってマージしたらこのような技術メモではなくて、真面目な文章にして公開したい。

元々のバンドル

元々のバンドルは esbuild で行っていた。
エントリポイントから依存するコードと node_modules 配下のコードを全て 1 つの JavaScript ファイルに吐き出していた。コードがそこそこ肥大化していたので、一応サイズは伏せるがだいぶ大きい JavaScript が 1 ファイルにまとめられて生成されていた。 style に関しては、CSS in JS に加え、グローバルなものは基盤となるテンプレートから直接読み込むようになっていた。

minify は esbuild の minify オプションを使っていた。
esbuild の minify は他のツールと遜色ないはずだが、完全に再現してくれるわけではないので別途 tarser などを入れた方がいいのではと感じていたが、現状で緊急性を感じてなかったので入れてなかった。

どうして試したか

1 番は自分が個人開発以外の環境で実際に試したかったからだが、真面目に理由を言語化すると以下のことが大きい。

  • 気軽に chunk 分割ができる(というより Vite 自体の機能として非同期の動的インポートによるファイル分割がサポートされている)
  • Vitest と一緒に使ってみたかった(まだやってない)

とはいえ、筆者は現状の esbuild でのバンドルによる開発者体験にだいぶ満足しているので、すごい導入したいかと言われるとそうではなく、どちらかというと個人的な興味であったり、手軽にプロダクション環境の最適化をすること、Vitest と一緒に使うならバンドルも Vite にしたいなというような背景の方が大きい。

構成

実際に試したアプリケーションの構成として、既存実装では PHP のテンプレートエンジンで吐き出した html に対して <script src="/path/to/index.js" /> のような形で esbuild でバンドルした JavaScript ファイルを読み込んでいる。
既存の PHP のテンプレートを使用する構成は壊さずに実装したいため、create-vite-app のような index.html が 1 枚あってそれを基準にバンドルするような構成ではなく、Backend Integration の部分に書いてあるような手法になる。

つまり

  • ローカルでは localhost:5173/path/to/index.tsx を探しにいく
  • 本番環境(ステージング環境)では任意の output dir に吐き出された manifest.json を利用してバンドルされた JavaScript と静的アセットを読み込む

のような方法になる。

vite.config.ts

特に難しいことはしていないが、以下のようになった。一部省略したりマスクしたりしてる箇所があるが、概ねこんな感じ。

import { defineConfig, splitVendorChunkPlugin } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import { hoge } from "./vite-plugin-hoge"; // この環境用に1つ入れてる

const isProduction = process.env.ENV === "production";
const OUT_DIR = path.resolve(__dirname, "/path/to/dist");

function getNodeEnv() {
  // 環境ごとの NODE_ENV を求める
}

function getAssetsPublicPath() {
  // 環境ごとの静的アセットの場所を返す
  // ここで指定したパスは本番ビルドをするときに build.outDir に吸収される
}

const config = defineConfig({
  publicDir: getAssetsPublicPath(),
  esbuild: {
    // ref https://github.com/vitejs/vite/issues/8644#issuecomment-1159308803
    logOverride: { "this-is-undefined-in-esm": "silent" },
    sourcemap: !isProduction,
    target: "es2022", // es2020 以上を指定しないと import.meta が使えない(最近 import.meta が無法地帯になってきてる感じがするがこれはまた別の話)
  },
  build: {
    outDir: OUT_DIR,
    manifest: true,
    rollupOptions: {
      // 本来ここで index.html を探しにいくが、上書きして React のエンドポイントを参照するようにする
      input: "./src/index.tsx",
    },
  },
  define: {}, // ここに環境変数が入る
  server: {
    hmr: {
      protocol: "ws",
    },
  },
  plugins: [react(), splitVendorChunkPlugin(), hoge()],
});

export default config;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

こんな感じで定義できる。

また、エントリポイントの PHP のテンプレートエンジンでは以下のように記述する。

<!-- ローカル環境での定義 -->
<script type="module">
  import RefreshRuntime from "http://127.0.0.1:5173/@react-refresh";
  RefreshRuntime.injectIntoGlobalHook(window);
  window.$RefreshReg$ = () => {};
  window.$RefreshSig$ = () => (type) => type;
  window.__vite_plugin_react_preamble_installed__ = true;
</script>
<script
  type="module"
  src="http://127.0.0.1:5173/src/index.tsx"
  crossorigin
></script>
1
2
3
4
5
6
7
8
9
10
11
12
13

若干ハマったところ

上の config を定義する中で、自分のケアレスミスも含めて若干ハマったところを書く。

hmr 用のプロトコルに ws を明示しないと wss をとりに行ってしまう

Vite は websocket を用いて hmr を実現する。
その際、何も指定しなければ、dev server と同じドメイン(デフォルトで localhost)ポート(デフォルトで 5173)で websocket 通信を行ってくれるが、その際のプロトコルはアプリケーションのプロトコルを見にいくようになっている。
コードで言うと vite/packages/vite/src/client/client.ts になる。

自分が今回手を入れた環境では、ローカルでも https 通信をするように設定されていて、そのため websocket の接続先も wss://localhost:5173 を探しに行ってしまい、うまくつながらなかった。
websocket は ws のまま通信して欲しかったので明示する必要があったと言うことだった。

ローカル環境を立ち上げた際に大量の警告が

yarn dev 等のローカル環境を立ち上げるコマンドを叩くと、大量の warning が出た。

9
20:58:56 [vite] warning: Top-level "this" will be replaced with undefined since this file is an ECMAScript module
154|                      value: value,
155|                      inputRef: ref,
156|                      __self: this,
   |                              ^
157|                      __source: {
158|                        fileName: _jsxFileName,

  Plugin: vite:esbuild
1
2
3
4
5
6
7
8
9
10

これは、どうやら esbuild のバージョンアップによって react の vite plugin が JSX の変換をする際に発生してるようで、現段階での回避策はコメントを省略することというコメント があったのでそれに従って設定した。

一部のモジュールが vite dev server 越しに取りに行けない

これはあまり解決方法がわからないが、JS/TS/img 以外(wasm など)のファイルが vite dev server 越しに取得できなかった。
そのため、ビルドする段階で読み取れない対象のファイルたちをあらかじめ out dir に移して静的ファイルとして扱って取得するようにした(out dir がローカル環境だとそのまま静的ファイルサーバとして扱える)

import fs from "fs";
import path from "path";
function moveToDist() {
  // ... 取得できないファイルたちを取得
  fs.copyFileSync(from, to); // dist に移動
}
1
2
3
4
5
6

本番ビルド

本番環境はまだあまりテコ入れしていないが、一旦 vite の用意してる splitVendorChunkPlugin でファイルの分割だけした。
従来のビルドは単一ファイルにコードを吐き出していて、ファイルサイズが大きかったが(具体的なサイズは一応伏せる)、Vite で脳死で本番環境用の JavaScript を吐き出したところ、エントリポイントのファイルが圧縮前の段階で 1/3 ほどになった。
これだけでもだいぶ小さくなったのでいいが、さらに色々改善できそうな部分が見えてるのでこれは今後一生懸命対応していきたい(Vite はまだお試し段階だが、Vite を導入するにしてもしないにしてもここのチューニングはある程度必要に感じているので)

ちなみに、本番ビルドで吐かれる manifest.json はこんな感じになる。
エントリポイントを基準にして書く key に辿れるようになっていて、これを PHP のテンプレートで読み込んで本番環境用のコードを実行することができる。

{
  "assets/images/logo.svg": {
    "file": "assets/logo.xxxxxx.svg",
    "src": "assets/images/logo.svg"
  },
  // ここがエントリポイントになるので、これを基準にして読んでいく
  "src/index.tsx": {
    "file": "assets/index.xxxxxx.js",
    "src": "src/index.tsx",
    "isEntry": true,
    "imports": ["_vendor.xxxxxx.js"],
    "css": ["assets/index.xxxxxx.css"],
    "assets": ""
  },
  "_vendor.xxxxxx.js": {
    "file": "assets/vendor.xxxxxx.js",
    "assets": ["assets/hoge.js"]
  },
  "src/index.css": {
    "file": "assets/index.xxxxxx.css",
    "src": "src/index.css"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

まとめ

興味駆動開発になってしまっているが、一旦形にはなったのでこれから拡張していく。

余談

なんだかんだ実践環境で初めて Vite を試してみた。個人開発では使っていたけど、特に config 書くことなかったし。
そこで感じたのが、ある程度わかっていて予想がつくことだがやはり初期ロードの際のネットワーク負荷が想像以上に重く、これは webpack がメモリに任せていたことをブラウザ(ネットワーク)に移しただけの状態では?と感じた。
とはいえ、hmr は爆速だし、体験も良いので満足はしている。しかし esbuild の爆速さにはやはり勝てないなと感じた。