こんにちは。PR TIMES フロントエンドエンジニアの岩元 (@yoiwamoto) です。
PR TIMES ではいくつかのページが React で実装されており、Webpack でビルドを行っていました。
今回は、一部のページを除いてこの Webpack を Vite へ置き換えたので、その経緯や結果を共有します。
まとめ
- ビルド時間が長いことが課題で移行を行い、結果として開発体験・デプロイ時間等が大幅に改善されました。
- 開発環境のみの移行 → フィーチャートグルでの本番試験 → リリース → Webpack の廃止と、移行は段階的に進めました。
なぜ Webpack をやめたのか
一番はやはり、ビルド時間の遅さです。
今回、当時の環境を再現することが難しく、改めて計測はできなかったのですが、本番用のビルドはおおよそ3~4分、開発環境での watch ビルドで変更が反映 (HMR) されるまで30秒~1分かかっていて、快適に開発が行える状態ではありませんでした。
これについては、開発環境での動作速度改善のため、一度 esbuild が導入されました。esbuild は Go 実装の高速なバンドラ (だけではありませんが) で、開発時の watch ビルドが1秒以内に終わるようになりました。

しかし、esbuild は HMR(画面のリロードを行わずに更新のあったモジュールのみを置換する)の機能は組み込まれておらず、これを esbuild 本体に実装する予定も、少なくとも直近ではないと Author が明言しています。
そのため、esbuild を使用する場合コードの変更のたびに画面をリロードすることになります。複数ステップがあるフォームや、モーダルの UI 変更などを行う際に、状態が毎回失われてしまうリロードは致命的でした。また、詳細は省略しますが、開発環境でページのリロードに数秒かかる場合も多くあり、ビルド自体が速くても、リロードに時間がかかってしまうというような状況がありました。
また、Webpack が今後、新しい課題に応えるような開発を積極的に行なっていく雰囲気ではなさそうということも、Webpack をやめることになった大きな理由の一つです。
Webpack founder でコアメンテナの sokra 氏も、現在は Vercel で Webpack の後継となる Turbopack の開発に取り組んでいるようです。

ちなみに、Turbopack は現時点では Next.js の開発環境で動作するよう作られていて単体では使用できません。もう少し時間がかかると思うので今回は検討していませんが、GA になったら候補にあがる可能性があるので、watch しておくつもりです。
なぜ Vite か
Vite は比較的新しいツールですが、既に一般的な選択肢になってきていて、あくまで目安ですが、State of JS 2022 でも TIER S に入るなど、存在感を示しています。

開発時にはブラウザネイティブの ESM を使用することで、バンドル無しで非常に高速に起動・変更を反映し、プロダクション用にはバンドルを生成します。
懸念であった、ビルド時間・開発時の HMR 反映のスピード・開発状況は、いずれも Vite を採用することでほぼ解決できるものであったため、採用を決めました。
導入の流れ
以下の順序で段階的に導入しました。
- 開発環境への導入
- プロダクションビルドの調整
- デプロイ job への組み込み
- フィーチャートグルで、本番で Webpack バンドルと Vite バンドルを切り替えられるように変更
- フラグを外して本番リリース
- デプロイ時の Webpack ビルド job を削除
開発環境への導入
まず、一番の課題であった開発環境の動作速度改善を目的に、開発環境に Vite を導入しました。
上述のように、当時は開発時には esbuild の watch ビルドを行っていたので、これを Vite の dev server に置き換えました。
Vite はデフォルトでは HTML をエントリポイントとして配信するようになっており、script に main.tsx
などを指定してここからモジュール解決を行います。
dev server での配信時にこの HTML を書き換えて、HMR に必要な @vite/client や react-refresh などを読み込むのですが、PR TIMES の現在のフロントエンドでは、HTML の配信をバックエンドサーバーで行なっているため、そのままでは利用できません。
以下の Backend Integration のガイドに従い、js をエントリポイントとして dev server を立ち上げ、バックエンドのテンプレートに必要な script を追記する対応を行い、導入しました。
この他、環境変数の読み込み・切り替えには ElMassimo/vite-plugin-environment を使用したのと、CSS に emotion を使用している都合上その設定を行いましたが、一般的なソリューションかと思うので、vite.config.ts の関連箇所の設定だけ貼るに留めておきます。
export default defineConfig({
plugins: [
react({
jsxImportSource: '@emotion/react',
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
EnvironmentPlugin('all'),
],
envDir: path.resolve(__dirname, 'config/env'),
envPrefix: 'PUBLIC_',
...otherConfig,
});
バンドルを行わないため dev server の起動は1秒未満、HMR も体感では瞬時に反映されるという状態になりました。毎回リロードして待っていたところから、開発体験は大きく改善されたと言えます。
プロダクションビルドの調整
次に、本番に投入していくため、プロダクションビルドの調整を行います。
vite build
でバンドルを行うと、デフォルトではエントリファイル名が ファイル名-<hash>.js
のようになります。先ほどの Backend Integration のページでは、このファイル名は、同時に吐き出される manifest.json に chunk などと合わせて記載があるので、バックエンドサーバーではその manifest.json を見て <script> の src を決めて HTML を生成する、とあります。
しかし、現在の構成では、フロントエンドとバックエンドのリポジトリを分けていることなどもあり、HTML のリクエスト時にバックエンドサーバーで manifest.json を参照する構成を作るのが少し面倒でした。
試験的な導入で、できるだけライトに実装したいということもあり、まずはエントリファイル名を固定することで、バックエンド側では固定のファイル名で参照できるようにしました。
関係のない設定は省略していますが、build の設定は以下のようになります。
export default defineConfig({
build: {
rollupOptions: {
input: {
'companyDetail': 'src/pages/companyDetail.tsx',
'price': 'src/pages/price.tsx',
...otherPages,
},
output: {
entryFileNames: '[name].js',
},
},
},
...otherConfig,
});
ただし、ファイル名を固定する場合はもちろん、変更前のキャッシュが読み込まれてしまわないように注意が必要です。
PR TIMES では一般的な手法として、デプロイごとに更新される ID をクエリパラメータで付与することでキャッシュバスターを行なっているため、フロントエンドのリリースのたびにバックエンドの空デプロイを実行することで、ユーザーがブラウザキャッシュ・CDN キャッシュなどから古い JavaScript を取得しないことを保証しています。
<script src="/path/to/bundle.js?v=<{$app_ver_id}>"></script>
ちなみに、自力でちゃんとバックエンドサーバーとの統合を行った事例としては、以下が非常に参考になります。

またこの時点で、パフォーマンス影響がないか、バンドルサイズの変化や、Vite バンドルを利用した時の Web Vitals の数値を計測しました。
結果として、バンドルサイズは1%程度増、Web Vitals は LCP が微増、他は変化なしでした。パフォーマンスだけを見ると改善とは言えないものの、メリットを打ち消すほどのクリティカルなものではないと判断しました。
デプロイ job への組み込み
これで、プロダクションビルドの動作に問題がないことがローカルで確認できたため、次に、ステージング・本番のデプロイ時に、既存の Webpack ビルドと合わせて Vite のビルドを行い、成果物を同時に S3 にアップロードするよう CI を更新しました。
デプロイするだけなので、この時点でユーザー影響はありません。
フィーチャートグルで、本番で Webpack バンドルと Vite バンドルを切り替えられるように変更
次に、実際に Vite バンドルで React アプリケーションが問題なく動作するか確認を行う必要があります。
ステージングでの挙動確認や、自動テストが通ったことをもって、本番リリースとするのも一つの手ですが、そうは言っても本番で何かが起こるという可能性もあります。今回のビルドツールの変更は、どこまで影響があるか ソフトウェアエンジニア・QA にとっても不明瞭な変更だったので、念のため、フィーチャートグルで本番試験を行うことにしました。
バックエンドに既にフィーチャートグルの機能があるので、ここに Webpack – Vite 切り替え用のフラグを追加し、このフラグを見て script を Webpack 用 – Vite 用で分岐するような処理を追加します。
フィーチャートグルは社内メンバーのみがアクセス可能なので、ユーザー影響を出さずに本番確認を行うことができました。
PR TIMES でのフィーチャートグルの詳細についはこちらのエントリもご覧ください。

フラグを外して本番リリース
本番で動作に問題がないことが確認できたので、最後に、フィーチャートグル用のフラグを削除し、全ユーザーが Vite のバンドルを読み込むよう変更を行いました。
リリースのタイミングですが、弊社では月に1度、主にレガシー改善を目的としたリファクタリングデーという日があり、この日にはステージング・本番でフルリグレッションテストが行われます。
今回、このフラグを外す変更は、影響範囲の広さも鑑みて、リファクタリングデーで合わせてフルリグレッションテストを行い、リリースしました。

デプロイ時の Webpack ビルド job を削除
最後に、デプロイ job が Webpack + Vite のビルドを直列で行って遅くなってしまっているので、不要になった Webpack のビルドを行わないよう修正します。
これによって、ビルドの job が Webpack 分の3~4分省略されて、30秒程度で終わるようになりました。
結果
dev server の起動:3~4分 → 1秒未満
開発時の変更の反映:フルリロードが必須 → 1秒未満の HMR
ビルド時間:3~4分 → 30秒
上記のように、各種数値が大幅に改善できました。
ビルド時間の短縮については、フロントエンドプレビュー環境の自動生成にも大きく貢献していいます。

一緒に働く仲間を募集しています!
開発本部では現在、フロントエンドエンジニアを含め、以下のような複数ポジションで積極的に採用を行っています。
- バックエンドエンジニア
- フロントエンドエンジニア
- デザイナー
- PdM
- QA エンジニア
- インフラエンジニア
- コーポレートエンジニア
また、先日 2/1 には、福岡にサテライトオフィスを開設しており、福岡での現地採用、および U・Iターン希望のエンジニアの採用も行なっていきます!

PR TIMES で働くことに興味を持っていただけた方は、ぜひ以下より情報をご確認ください!