メインコンテンツへスキップ
#Web制作 約9分で読めます

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.jsonsideEffects フィールドの見直し。上流パッケージで 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 buildnext build --turbopack を CI で並列実行し、First-load JS の差分を監視する。差が縮まってきたら切り替える判断材料になる

Next.js 16 では Turbopack がデフォルトバンドラーになり、16.1 で Persistent Caching が stable になっている。15系にこだわる理由がなければ、16系に上げてから Turbopack に移行するほうがトラブルは少ないと思う。

ビルドの速さは正義だが、出力されるバンドルの品質が伴わないと本末転倒になる。切り替える前に、自分のプロジェクトで計測。それだけは省略しないほうがいい。

参考リンク

#Next.js #Turbopack #Webpack #ビルド #移行
シェア