はじめに
こんにちは。開発部の竹内です。
弊社のプロダクトの1つである モビマル におけるフロントエンドビルドツール刷新の取り組みについてご紹介します。具体的には webpack から Rspack への移行を行いましたので、手順や結果をお伝えしたいと思います。
既存のプロジェクトの構成
モビマルはRailsアプリケーションとして構築されており、Reactで書かれたフロントエンド部分はwebpackでバンドルされ、app/assets/ディレクトリに出力されていました。その後、アセットパイプラインを通して利用するというワークフローになっていました。
ですが、この構成には以下の課題がありました。
- webpack とアセットパイプラインの役割が重複し無駄になっている
- 開発環境で Source Map が二重に出力されてブラウザから利用できなくなっている
- 開発環境で HMR(Hot Module Replacement) ができない
特に HMR ができない課題は、変更のたびにページのリロードが必要になり、開発者体験を落とす結果に繋がっていました。
そこでフロントエンドビルドツールのモダン化を行い、開発効率・開発者体験の向上を図りました。
- 脱アセットパイプライン
- webpack-dev-server の導入(HMR の実現)
- Rspack への移行
の三段階に分けて対応を進めました。
具体的な対応内容について以降のセクションで紹介していきます。
脱アセットパイプライン
webpack でビルドした bundle ファイルはアセットパイプラインを通さず直接 view から読み込むことを目標とします。
client/ ディレクトリにあるフロントエンドのコードをビルドして public/packs/ に出力し、 view ではそこから script タグを用いて読み込むように変更します。
ここで問題になるのがキャッシュコントロールのため必要となるフィンガープリントです。アセットパイプラインを使えば javascript_include_tag でフィンガープリントが自動で付与されますが、webpack で出力した場合は javascript_include_tag を使用できません。
そこで以下の記事を参考にwebpack-manifest-plugin を用い manifest.json を出力し、独自にフィンガープリント付きのパスで script タグを出力する helper を作成しました。
webpack-dev-server の導入
参考記事 と同様に webpack-dev-server の設定を行いました。これにより React の HMR が可能になり、コード変更時のページリロードが不要になります。具体的な変更内容は以下です。
- webpack.config.js の設定
- helper の変更
- プロキシの設定
- react-refresh-webpack-plugin の導入
これらの設定により HMR を実現できましたが、筆者の環境では webpack での HMR に10〜15秒程度の時間を要し、まだ改善の余地があると感じました。
上記を含めたコード例は次のセクションでまとめて掲載します。
Rspack への移行
フロントエンドビルド環境のモダン化を進めるにあたり、高速ビルドツールとしてViteとRspackの二つの選択肢を検討しました。両者ともに十分なビルド速度を備えていましたが、違いを比較した結果僅差ではありますが Rspack を選択しました。
| Vite | Rspack |
|---|---|
| 設定がシンプル。 | 設定は webpack と互換性が高い(やや複雑になる可能性がある)。 |
| HMR 対応のためプロダクションコードの変更が必要になる。 | 現在のプロダクションコードで HMR 対応可能。 |
| webpack から移行する場合の(Railsに統合するための)修正が多め。 | webpack から移行する場合の修正が少ない。 |
| 採用実績は多め。 | 1.0が出たのが2024年8月なのでまだ採用実績は少ない。 |
当初は Rails から使用するための参考資料が多めの Vite への移行を想定していました。
ですが現在 webpack を使用しているため移行コストの低い Rspack を選択するメリットは多いと考え、こちらの採用を決定しました。
まずは必要なライブラリをインストールします
yarn add -D @rspack/core @rspack/cli @rspack/dev-server @rspack/plugin-react-refresh @swc/helpers react-refresh rspack-manifest-plugin yarn add postcss
webpack.config.js の代わりに rspack.config.js を設定します。
const path = require('path') const { rspack } = require('@rspack/core') const { RspackManifestPlugin } = require('rspack-manifest-plugin') const ReactRefreshPlugin = require('@rspack/plugin-react-refresh') // HMR を可能にする module.exports = (env, argv) => { const isDevelopment = argv.mode === 'development' return { output: { path: path.resolve(__dirname, '../public/packs/'), filename: '[name]/webpack/bundle-[contenthash].js', // フィンガープリントをファイル名に含める publicPath: '/packs/', }, entry: { 'managers/offer_projects/opening_shifts': './src/entryPoints/Managers/OfferProjects/OpeningShift/index.tsx', // ... }, resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx'], modules: [path.resolve(__dirname, 'src'), 'node_modules'], }, devtool: argv.mode === 'development' ? 'source-map' : false, module: { rules: [ { test: /\.(j|t)sx?$/, exclude: [/[\\/]node_modules[\\/]/], loader: 'builtin:swc-loader', // 高速化のため babel-loader ではなく、推奨されている builtin:swc-loader を使用 options: { jsc: { parser: { syntax: 'typescript', tsx: true, }, externalHelpers: true, transform: { react: { runtime: 'automatic', development: isDevelopment, refresh: isDevelopment, }, }, }, env: { targets: 'defaults', }, }, }, { test: /\.css$/, use: ['style-loader', 'css-loader'], type: 'javascript/auto', }, ], }, plugins: [ // 無駄な locale を読み込まないようにする new rspack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }), new RspackManifestPlugin({ // manifest.json を出力 fileName: 'manifest.json', publicPath: '/packs/', writeToFileEmit: true, }), ].concat(isDevelopment ? [new ReactRefreshPlugin()] : []), devServer: { host: '0.0.0.0', port: 21201, headers: { 'Access-Control-Allow-Origin': '*', }, allowedHosts: [ 'localhost', 'client', // docker-compose のサービス名が client なので、コンテナ内からアクセスできるようにする ], }, } }
script タグを生成する helper(javascript_bundle_tag) を作成します。
module GlobalHelpers module WebpackBundleHelper class BundleNotFound < StandardError; end # manifest.json を読み込んで、エントリーポイント名からファイルパス(client でビルドしたハッシュ付きファイル名)を取得する # @see https://inside.pixiv.blog/subal/4615#%E3%83%93%E3%83%A5%E3%83%BC%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC%E3%81%AE%E5%AE%9F%E8%A3%85 def asset_bundle_path(entry, **options) return '' if Rails.env.test? # テスト環境では manifest.json がない可能性があるので空文字列を返す raise BundleNotFound, "Could not find bundle with name #{entry}" unless webpack_manifest.key? entry asset_path(webpack_manifest.fetch(entry), **options) end def javascript_bundle_tag(entry, **options) path = asset_bundle_path("#{entry}.js") options = { src: path, }.merge(options) javascript_include_tag '', **options end private MANIFEST_PATH = 'public/packs/manifest.json' # 開発環境・テスト環境以外では manifest.json を1度だけ読み込む MANIFEST_CONTENT = Rails.env.development? || Rails.env.test? ? nil : JSON.parse(File.read(MANIFEST_PATH)) def webpack_manifest if Rails.env.development? # dev-serverから取得する JSON.parse(OpenURI.open_uri("http://#{Settings.dev_server.host}/packs/manifest.json").read) else MANIFEST_CONTENT end end end end
dev-serverからのアセット取得のプロキシ設定です。参考記事 ほぼそのままです。
require 'rack/proxy' # dev-serverからのアセット取得をプロキシする -> localhost以外からもdev環境を見れるようにするため # @see https://studist.tech/goodbye-webpacker-183155a942f6 class DevServerProxy < Rack::Proxy def perform_request(env) if env['PATH_INFO'].start_with?('/packs/') env['HTTP_HOST'] = dev_server_host env['HTTP_X_FORWARDED_HOST'] = dev_server_host env['HTTP_X_FORWARDED_SERVER'] = dev_server_host super else @app.call(env) end end private def dev_server_host Settings.dev_server.host end end Rails.application.configure do # ... config.middleware.use DevServerProxy, ssl_verify_none: true end
package.json のビルドコマンドを rspack に変更します。
{
...
"scripts": {
- "build": "webpack --mode=development",
- "build:prod": "webpack --mode=production",
- "watch": "webpack --mode=development --watch"
+ "build": "rspack --mode=development",
+ "build:prod": "rspack --mode=production",
+ "dev-server": "rspack serve --hot --mode development"
},
...
あとは各 view で javascript_bundle_tag を使うように書き換えれば対応完了です。
HMR を試したところ1秒以内に完了するため快適になりました。
まとめ
Rspack の導入で高速な HMR が可能になり、開発効率・開発者体験が向上しました。
今はモビマルのみ Rspack を使用していますが、他のプロダクトでも展開を検討しています。
Rails アプリケーションにおける Rspack の導入事例はまだウェブ上でも多く見られなかったため、本記事が参考になれば幸いです。