Next.jsプロジェクトで開発サーバーの起動が遅い、HMR(Hot Module Replacement)の反映に数秒かかる——そんな悩みを抱えているエンジニアの方は少なくないのではないでしょうか。特にプロジェクトが成長し、コンポーネントやページが増えるほど、この問題は深刻になっていきます。「保存してからブラウザに反映されるまでの数秒」が、1日に何百回も繰り返されることを考えると、開発体験への影響は想像以上に大きいものです。
本記事では、Next.jsが採用する2つのバンドラー、TurbopackとWebpackを実践的に比較し、プロジェクトに合った選択の指針をお伝えします。単なるベンチマーク比較にとどまらず、実際の開発現場で直面した課題や、移行時のトラブルシューティングも含めてお伝えしますので、導入検討の参考にしていただければ幸いです。
TurbopackとWebpackの基本的な違い
Webpackが築いた10年の歴史
Webpackは2014年の登場以来、フロントエンド開発のデファクトスタンダードとして広く使われてきました。豊富なプラグインエコシステムと高いカスタマイズ性が魅力ですが、プロジェクト規模が大きくなるにつれてビルド速度の低下が課題になることがあります。私自身、数百コンポーネントを超える規模のプロジェクトで、開発サーバーの起動に30秒以上かかる状況に直面した経験があります。
Webpackがここまで普及した背景には、それ以前のフロントエンド開発が抱えていた課題がありました。複数のJavaScriptファイルを<script>タグで読み込み、グローバル変数の衝突に悩まされていた時代から、モジュールバンドルという概念を一般化させた功績は大きいです。コード分割(Code Splitting)、Tree Shaking、各種アセットの統合処理など、現在のフロントエンド開発で当たり前となっている機能の多くは、Webpackが切り拓いたものといえます。
Turbopackの登場と設計思想
Turbopackは、Webpack作者のTobias Koppers氏がVercelで開発したRust製バンドラーです。Next.js 15で開発サーバー向けに安定版となり、next dev --turbopackで利用できるようになりました。最大の特徴はインクリメンタル計算エンジンで、変更されたモジュールとその依存関係だけを再計算する設計になっています。
Turbopackの内部で採用されている「Turbo Engine」は、関数レベルでの細粒度キャッシュを実現しています。たとえば、あるコンポーネントのスタイルだけを変更した場合、Webpackではそのコンポーネントを含むチャンク全体を再ビルドする必要がありますが、Turbopackは変更されたスタイル処理の関数結果だけを再計算し、残りはキャッシュから取得します。この仕組みにより、プロジェクト全体のモジュール数が増えても、HMRの速度は変更の影響範囲にのみ比例するようになっています。
アーキテクチャの根本的な違い
両者の根本的な違いは、WebpackがJavaScriptで依存グラフ全体を処理するのに対し、TurbopackはRustの並列処理性能を活かして関数レベルでキャッシュ・再計算を行う点にあります。この設計思想の違いが、特に大規模プロジェクトでの速度差として顕著に表れます。
もう少し具体的に説明すると、Webpackは依存関係グラフを構築する際に、エントリーポイントからすべてのimport文を辿り、ファイル単位でモジュールを解決します。この処理はシングルスレッドのJavaScriptで行われるため、CPUコアが多いマシンでもその恩恵を十分に受けられません。一方、TurbopackはRustのマルチスレッド処理を活用し、依存関係の解決・トランスパイル・バンドルの各フェーズを並列に実行します。8コアのマシンであれば、理論上は8つのモジュールを同時に処理できるわけです。
私がとくに印象的だったのは、Turbopackの「遅延コンパイル(Lazy Compilation)」の挙動です。開発サーバー起動時にすべてのページをコンパイルするのではなく、実際にブラウザからアクセスされたルートだけをオンデマンドでコンパイルします。100ページあるプロジェクトでも、開発中に触るのは数ページだけということは珍しくありません。この仕組みのおかげで、プロジェクト規模に関わらず起動が高速になります。
開発速度の実測比較
同じように気になっている方も多いと思いますので、具体的な数値で比較してみます。以下は、約500モジュールを持つNext.js 15プロジェクトでの計測結果です。
| 計測項目 | Webpack | Turbopack | 改善率 |
|---|---|---|---|
| 開発サーバー初回起動 | 28.4秒 | 4.2秒 | 約85%高速化 |
| HMR(コンポーネント変更) | 1.8秒 | 0.12秒 | 約93%高速化 |
| HMR(CSS変更) | 0.9秒 | 0.06秒 | 約93%高速化 |
| ルート初回アクセス | 3.2秒 | 0.8秒 | 約75%高速化 |
Vercelの公式ベンチマークでは、大規模アプリケーションにおいてHMRが最大96%高速化すると報告されています。私の計測でも、モジュール数が増えるほどTurbopackの優位性が際立つ傾向を確認できました。
計測環境と方法について
公平を期すために、計測環境も記載しておきます。マシンはApple M2 Pro(12コア、32GB RAM)、Node.js v20.11、Next.js 15.1を使用しました。各計測は5回実行した中央値を採用しています。また、OSのディスクキャッシュの影響を排除するため、各計測前にnode_modules/.cacheと.nextディレクトリを削除しています。
HMRの計測は、performance.now()を使ってファイル保存からブラウザ上の変更検知までの時間を測定しました。具体的には、Reactコンポーネント内にタイムスタンプを埋め込み、HMR更新イベントのコールバックで差分を計算する方法です。手動でストップウォッチを使うよりも正確な数値が得られますので、ご自身のプロジェクトでも試してみてください。
プロジェクト規模別の傾向
一方、50モジュール未満の小規模プロジェクトでは、Webpackでも初回起動が3〜5秒程度に収まるため、体感差は小さくなります。最初はTurbopackの恩恵を感じにくかったのですが、プロジェクトが成長するにつれて差が開いていくことに気づかされました。
規模別の傾向をもう少し詳しくまとめると、以下のようになります。
- 小規模(〜50モジュール):Webpackでも十分快適。Turbopackの導入メリットは限定的で、どちらを使っても開発体験に大きな違いはありません
- 中規模(50〜300モジュール):WebpackのHMRが体感で1秒を超え始める段階。Turbopackに切り替えると「保存した瞬間に反映される」感覚が得られ、開発リズムが明らかに変わります
- 大規模(300モジュール〜):Webpackでは起動に20秒以上かかることも珍しくなく、Turbopackの恩恵が最も大きい領域です。私が経験した1,200モジュール規模のプロジェクトでは、Webpackの起動が45秒だったのに対し、Turbopackでは6秒まで短縮されました
メモリ使用量とリソース消費の比較
速度だけでなく、メモリ使用量も開発環境の快適さに影響する重要な指標です。とくにDocker上で開発している場合や、スペックの限られたマシンを使っている場合、メモリ消費の違いは無視できません。
先ほどの500モジュールのプロジェクトで、開発サーバー稼働中のメモリ使用量を比較しました。
| 計測項目 | Webpack | Turbopack |
|---|---|---|
| 起動直後のRSS | 約420MB | 約280MB |
| 30分間の開発作業後 | 約680MB | 約350MB |
| HMR中のピーク使用量 | 約750MB | 約400MB |
Turbopackの方がメモリ効率が良い理由は、Rustのメモリ管理モデルにあります。JavaScriptのガベージコレクションに依存するWebpackと異なり、Rustではコンパイル時にメモリの割り当てと解放が決定されるため、不要なメモリが長時間残留することがありません。
ただし、注意点もあります。TurbopackはRustバイナリとして動作するため、Node.jsのプロセスとは別にメモリを消費します。topコマンドやActivity Monitorで確認する際は、Node.jsプロセスだけでなくTurbopackのプロセスも含めて合算する必要があります。私も最初はNode.jsプロセスだけを見て「あまり変わらない」と勘違いしていたのですが、合算するとTurbopackの方が明確に少ないことがわかりました。
設定とカスタマイズの観点での比較
Webpackの大きな強みは、成熟したプラグインエコシステムです。next.config.jsのwebpack関数でローダーやプラグインを自由に追加できます。
// next.config.js — Webpack設定の例
module.exports = {
webpack: (config, { isServer }) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};Turbopackでは、next.config.jsのturbopackオプションで設定します。Webpack互換のローダーも一部サポートされていますが、すべてのプラグインが動作するわけではありません。
// next.config.js — Turbopack設定の例
module.exports = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
'@components': './src/components',
},
},
};正直なところ、AとBで悩む場面はカスタムWebpack設定を多用しているプロジェクトほど多くなります。以下のチェックリストで移行可否を判断できます。
Turbopack移行判断チェックリスト
- カスタムWebpackプラグインを使用していないか
- カスタムローダーがTurbopackでサポートされているか
-
next.config.jsのwebpack関数の設定内容を棚卸ししたか - Node.js依存の特殊なビルド処理がないか
- CI/CDパイプラインでの
next buildはWebpack前提で問題ないか(Turbopackのビルドは2025年時点でまだ安定化途上)
主要ローダー・プラグインの対応状況
移行を検討する際に気になるのが、普段使っているローダーやプラグインがTurbopackで動くかどうかです。私が実際に確認した範囲で、主要なものの対応状況をまとめました。
| ローダー/プラグイン | Turbopack対応 | 備考 |
|---|---|---|
@svgr/webpack | ○ | turbopack.rulesで設定可能 |
css-loader / postcss-loader | ○ | 組み込みでサポート |
sass-loader | ○ | Next.jsのsassOptions経由で利用 |
babel-loader | △ | SWCが代替として動作。Babelプラグインに依存する場合は注意 |
webpack-bundle-analyzer | × | Turbopack用の代替手段は現時点でなし |
copy-webpack-plugin | × | 別の方法での対応が必要 |
DefinePlugin | ○ | env設定で代替可能 |
「×」のプラグインを使っている場合でも、開発サーバーだけTurbopackに切り替え、ビルド時はWebpackを使う構成であれば問題ないケースがほとんどです。webpack-bundle-analyzerはビルド時に使うものですし、copy-webpack-pluginもビルド工程で動作すれば十分な場合が多いでしょう。
CSS・スタイリング周りの対応状況
フロントエンド開発において、スタイリングは避けて通れない領域です。CSSの処理はバンドラーの挙動に直結するため、Turbopackへの移行時には特に注意が必要です。
CSS Modulesの挙動
CSS Modulesは、TurbopackとWebpackの両方で問題なく動作します。ただし、細かい挙動に違いがある場合があります。私が遭遇したケースでは、CSS Modulesのcomposes構文を使った継承で、Webpackでは動作していたパターンがTurbopackではエラーになることがありました。
/* components/Button.module.css */
.base {
padding: 8px 16px;
border-radius: 4px;
font-weight: 600;
}
.primary {
composes: base;
background-color: #0070f3;
color: white;
}このような基本的なcomposesは問題ありませんが、別ファイルからのcomposes(composes: base from './shared.module.css')では、ファイルの解決順序の違いにより警告が出ることがあります。もし同様の問題に遭遇した場合は、composesを使わずにクラス名を複数指定する方法に書き換えるのが確実です。
Tailwind CSSとの相性
Tailwind CSSを使用しているプロジェクトでは、Turbopackとの相性は非常に良好です。PostCSSの処理が組み込みでサポートされているため、特別な設定なしにTailwind CSSが動作します。むしろ、Tailwind CSSのJITモード(Just-In-Time)とTurbopackの遅延コンパイルは設計思想が似ており、必要なクラスだけを生成する仕組みとの相性が優れています。
// postcss.config.js — TurbopackでもWebpackでも同じ設定で動作
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};CSS-in-JSライブラリの注意点
styled-componentsやEmotionなどのCSS-in-JSライブラリを使用している場合は、少し注意が必要です。これらのライブラリはランタイムでCSSを生成するため、バンドラーの影響を受けにくい面がありますが、SSR時のスタイル抽出処理でバンドラー依存のコードが含まれていることがあります。
styled-componentsを使用している場合、Next.jsのcompiler.styledComponentsオプションを有効にしていれば、TurbopackでもWebpackでも同じように動作します。ただし、Babelプラグイン(babel-plugin-styled-components)に依存した設定をしている場合は、SWCベースの設定に書き換える必要があります。
TypeScript・パスエイリアスの挙動差
TypeScriptプロジェクトでの挙動についても触れておきます。基本的な型チェックとトランスパイルは両者とも問題なく動作しますが、いくつか知っておくべき違いがあります。
tsconfig.jsonのpathsとの連携
tsconfig.jsonのpaths設定は、TurbopackとWebpackの両方で認識されます。ただし、Webpackではtsconfig-paths-webpack-pluginを使ってパスエイリアスを解決するのに対し、TurbopackはNext.jsの設定から直接パスを解決します。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@lib/*": ["./src/lib/*"]
}
}
}この設定だけであれば、Turbopackへの移行で問題が起きることはほぼありません。ただし、next.config.jsのwebpack関数内で独自のresolve.aliasを設定している場合は、それに対応するturbopack.resolveAliasの設定が必要です。
型エラーとビルドエラーの検出タイミング
Webpackの開発サーバーでは、型エラーがあってもコンパイル自体は成功し、ブラウザにオーバーレイで型エラーが表示されます。Turbopackでも同様の挙動ですが、エラーオーバーレイの表示がより速いため、タイポなどの単純なミスにすぐ気づけるようになりました。些細なことに思えるかもしれませんが、「エラーに気づく→修正する→反映を確認する」のサイクルが速くなることは、日々の開発効率に確実に効いてきます。
段階的な移行戦略と注意点
Turbopackへの移行で一概には言えない部分もありますが、実務で有効だった段階的アプローチを紹介します。
ステップ1:開発サーバーのみ切り替え
package.jsonのdevスクリプトを変更するだけで試せます。本番ビルドはWebpackのままなのでリスクが低い方法です。
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build"
}
}ステップ2:互換性の確認
開発サーバーで一通りの画面を確認し、表示崩れやエラーがないかチェックします。特にCSS Modules、Sass、画像インポートまわりは動作確認が重要です。問題が出た場合はturbopack設定でローダーを追加するか、一旦Webpackに戻して対応を検討します。
確認すべきポイントをもう少し具体的に挙げると、以下の項目を重点的にチェックするとよいでしょう。
- 動的インポート(
next/dynamic):遅延読み込みコンポーネントが正しくレンダリングされるか - 画像最適化(
next/image):ローカル画像のインポートパスが正しく解決されるか - フォント(
next/font):Google FontsやローカルフォントのCSS生成に問題がないか - API Routes / Route Handlers:サーバーサイドのコードがエラーなく動作するか
- ミドルウェア(
middleware.ts):リダイレクトや認証処理が期待どおりに動作するか
ステップ3:チーム全体への展開
問題がなければチーム全体のdev環境をTurbopackに切り替えます。振り返ると、最初に小さなプロジェクトで試してからメインプロジェクトに適用するのが、遠回りに見えて最も確実な方法でした。
チームへの展開時には、package.jsonの変更だけでなく、READMEやチームのドキュメントにも一言添えておくと親切です。「Turbopackで動かない場合はnext dev(--turbopackなし)で従来どおり起動できます」といったフォールバック手順を明記しておくだけで、トラブル時の混乱を大幅に減らせます。
注意点として、2025年4月時点でnext buildのTurbopack対応はまだベータです。本番ビルドについてはWebpackを継続利用し、Turbopackの安定化を待つのが現実的な判断かもしれません。
実際のプロジェクトで遭遇した問題と解決策
ここまでの説明だけでは「結局うまくいくの?」という不安が残るかもしれません。私が実際にTurbopackへの移行で遭遇した問題と、その解決策を共有します。
問題1:Module not foundエラー
移行直後に最も多く遭遇したのがこのエラーです。Webpackでは暗黙的に解決されていたモジュールパスが、Turbopackでは厳密に解決されるケースがありました。
たとえば、拡張子を省略したインポートがWebpackでは動作していたのに、Turbopackではエラーになることがありました。
// Webpackでは動作するがTurbopackではエラーになる場合がある
import { utils } from './helpers';
// 明示的に拡張子を指定すると解決
import { utils } from './helpers/index';対処法としては、TypeScriptのmoduleResolution設定を確認し、インポートパスを明示的にすることで解決できます。この修正はWebpackでも問題なく動作するため、コードの品質向上にもつながります。
問題2:環境変数の読み込みタイミング
.env.localの環境変数がTurbopackでは異なるタイミングで読み込まれるように感じたことがありました。調べてみると、環境変数自体の読み込みはNext.jsが行うためバンドラーに依存しないのですが、HMRが高速なぶん、環境変数を変更した後の反映が「速すぎて気づかない」ことがありました。Webpackでは再起動が必要だった場面で、Turbopackは即座に反映されていたのです。これは問題ではなくむしろ利点でしたが、最初は混乱しました。
問題3:サードパーティライブラリとの互換性
特定のサードパーティライブラリが、Turbopackの開発サーバーで正しく動作しないケースに遭遇しました。私の場合は、あるグラフ描画ライブラリがブラウザのグローバル変数windowに依存しており、TurbopackのSSR処理でwindow is not definedエラーが発生しました。
// 問題が起きたコード
import Chart from 'some-chart-library';
// next/dynamicで動的インポートに変更して解決
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('some-chart-library'), { ssr: false });この対処法はWebpackでも有効なベストプラクティスなので、Turbopack移行をきっかけにコードが改善されたともいえます。
CI/CDパイプラインとの統合における考慮点
開発環境でTurbopackを採用する場合、CI/CDパイプラインとの関係も整理しておく必要があります。
開発とビルドでバンドラーが異なる構成
前述のとおり、2025年4月時点では「開発はTurbopack、ビルドはWebpack」という構成が現実的です。この構成で気をつけるべきは、開発時には問題なく動作するコードがビルド時にエラーになる(またはその逆)可能性があることです。
私のチームでは、CIパイプラインに以下の2つのチェックを組み込むことで、この問題に対処しています。
# GitHub Actionsの例
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Type Check
run: npx tsc --noEmit
- name: Build Check (Webpack)
run: npm run build型チェックとビルドチェックを分けることで、バンドラーに起因するエラーと型エラーを区別しやすくなります。
将来的なTurbopackビルドへの備え
Turbopackのビルド対応が安定版になった際にスムーズに移行できるよう、next.config.jsの設定をWebpack固有のものとTurbopack共通のものに整理しておくことをお勧めします。具体的には、webpack関数内のカスタム設定を可能な限り減らし、Next.jsの組み込み機能やturbopackオプションでも表現できる形に近づけておくと、将来の移行コストを下げられます。
まとめ:プロジェクト特性に合わせた選択を
Turbopackは、特に中〜大規模プロジェクトの開発体験を劇的に改善するポテンシャルを持っています。一方で、Webpackの豊富なエコシステムと安定性は、複雑な設定が必要なプロジェクトでは依然として頼れる存在です。
ここまでの内容を踏まえて、判断の目安を整理します。
Turbopackを積極的に採用すべきケース:
- 新規のNext.js 15プロジェクト
- カスタムWebpack設定が少ない、またはないプロジェクト
- モジュール数が100を超え、HMRの遅さが開発効率に影響しているプロジェクト
- Tailwind CSSやCSS Modulesなど、標準的なスタイリング手法を使っているプロジェクト
Webpackを継続すべきケース:
- Turbopack未対応のカスタムプラグインに強く依存しているプロジェクト
- Babelプラグインによる独自のコード変換が必要なプロジェクト
- ビルド時の
webpack-bundle-analyzer等によるバンドル分析が運用に不可欠なプロジェクト
私の実感としては、新規プロジェクトや設定がシンプルなプロジェクトではTurbopackを積極的に採用し、カスタム設定が多い既存プロジェクトでは段階的に移行するのが良いバランスだと感じています。
Turbopackはまだ進化の途上にあり、アップデートのたびに対応範囲が広がっています。今すぐ完全移行できなくても、開発サーバーだけでも切り替えてみることで、日々の開発体験は確実に向上するはずです。まずはnext dev --turbopackを試すところから始めてみてはいかがでしょうか。
フロントエンド開発の高速化やNext.jsプロジェクトの技術選定でお悩みでしたら、お問い合わせからお気軽にご相談ください。実際のプロジェクト構成に合わせた最適な構成をご提案いたします。
