Next.js 15 の本番ビルドを Turbopack に切り替えた記録
Webpack で動いていた Next.js 15 プロジェクトの本番ビルドを Turbopack に移行し、ビルド速度・バンドルサイズ・CSS崩れの3点で何が起きたかを記録する
next build --turbopack が beta になったとき、正直「dev では速いけど本番はまだ先だろう」と思っていた。Next.js 15.5 で Vercel が自社サイト(vercel.com、v0.app、nextjs.org)を Turbopack ビルドに載せて12億リクエスト超を捌いていると発表したので、手元のプロジェクトでも試してみることにした。
結論から言うと、ビルドは確かに速くなる。ただしバンドルサイズとCSSの並び順で想定外のことが起きるので、計測なしに切り替えるのはやめたほうがいい。
ビルド速度:体感で2〜3割は速い
手元の中規模プロジェクト(App Router、ルート40本くらい、Next.js 15.5)で Webpack と Turbopack のコールドビルドを比較した。.next を毎回消して3回ずつ回した程度のざっくり計測だが、Turbopack のほうがだいたい2〜3割速かった。
Catch Metrics が Cal.com(ルート153本)で出したベンチマークでも、コールドビルドの中央値が Webpack 187秒 → Turbopack 152秒で約19%短縮という数字が出ている。コア数が多いマシンほど差が開く設計で、Vercel の公式ブログでは30コアのマシンで最大5倍という数字も載っている。
ただし小さいプロジェクトだと Webpack のディスクキャッシュが効くぶん、差はほとんど出ない。2回目以降のビルドでは Webpack のほうが速いケースすらあった。Turbopack にも Persistent Caching の仕組みが入りつつあるが、Next.js 15.5 時点ではまだ実験的な扱い。
バンドルサイズ:共有チャンクが膨らむ問題
ビルド速度より気になったのがバンドルサイズの変化。先ほどの Catch Metrics の Cal.com ベンチマークでは、App Router の共有クライアントチャンクが Webpack の180KB から Turbopack で391KB に膨らんでいる。211KB増、率にして117%。全153ルートのFirst-load JSが例外なく増加し、中央値で+279KB(+72%)。
自分のプロジェクトでも似た傾向で、ルートごとのFirst-load JSがざっくり数十KB増えていた。LCPへの影響は手元の環境(有線+デスクトップ)ではほぼ体感できなかったが、モバイルの中速回線だと無視できない差になるはず。
原因は tree-shaking のヒューリスティクスの違い。Turbopack は Webpack とチャンク分割のロジックが異なり、共有バンドルに含めるモジュールの判断基準が違う。公式もこの差を認識していて「stable リリースまでにギャップを埋める」としているが、15.5 時点では注意が必要。
本番投入前にやるべきことは1つ。next build の出力に表示される各ルートの First-load JS を Webpack ビルドと比較する。増分が許容範囲かどうかはプロジェクトの性質とターゲットデバイス次第。
CSS の並び順が変わる:見た目が崩れる原因
移行で一番厄介だったのが CSS の適用順序。Turbopack は CSS ファイルの結合順が Webpack と異なることがあり、CSS Modules を多用しているプロジェクトで見た目が崩れた。
具体的には、sideEffects: false が設定されたパッケージの CSS インポートが Turbopack 側で省略されたり、結合順が入れ替わったりする。Webpack では問題なく動いていたスタイルが、Turbopack だと別のルールに負けて上書きされる。
対処法は主に2つ。
1つ目は、CSS Module 内で @import を使って依存順を明示する方法。
/* button.module.css */
@import './utils.module.css';
.button {
/* utils.module.css の定義が先に来ることが保証される */
}
2つ目は、package.json の sideEffects フィールドの見直し。上流パッケージで sideEffects: false が不正確に設定されているケースが多く、CSS ファイルが副作用なしと判定されて除外される。自分のプロジェクトの package.json で明示的に CSS を副作用ありとマークする。
{
"sideEffects": ["*.css", "*.module.css"]
}
next.config の書き換え:webpack → turbopack
Webpack のカスタム設定を Turbopack に移す作業も必要になる。Turbopack は Webpack プラグインをサポートしていないので、プラグインに依存している設定は代替手段を探すことになる。
ローダーについては、turbopack.rules に書き直す。よく使う @svgr/webpack はそのまま動く。
// next.config.js
module.exports = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
// webpack の resolve.alias に相当
'@components': './src/components',
},
},
}
注意点として、Turbopack のローダーは JavaScript を返すものしかサポートしていない。画像やスタイルシートを変換するタイプのローダーは動かない。また、ローダーのオプションに require() でプラグインモジュールを渡すパターンも使えない。
移行時のコツは、まず turbopack キーなしの素の状態でビルドを通し、エラーが出たローダーだけを turbopack.rules に追加していくこと。一度に全部移そうとすると、どの設定が壊しているのか切り分けられなくなる。
段階的な導入がいい
今の時点で「全部 Turbopack に切り替えろ」とは言えない。現実的なのは段階的な導入。
- dev は Turbopack:Next.js 15 でデフォルトになっている。HMR が体感で倍以上速い。これを使わない理由はない
- build は Webpack のまま:バンドルサイズの差と CSS 順序の問題が解消されるまでは、本番ビルドは Webpack で安定運用するのが手堅い
- CI で両方回す:
next buildとnext build --turbopackを CI で並列実行し、First-load JS の差分を監視する。差が縮まってきたら切り替える判断材料になる
Next.js 16 では Turbopack がデフォルトバンドラーになり、16.1 で Persistent Caching が stable になっている。15系にこだわる理由がなければ、16系に上げてから Turbopack に移行するほうがトラブルは少ないと思う。
ビルドの速さは正義だが、出力されるバンドルの品質が伴わないと本末転倒になる。切り替える前に、自分のプロジェクトで計測。それだけは省略しないほうがいい。