OpenAINext.js2019/02/19 14:00

Next.js 8 Webpack Memory Improvements

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Next.js 8 の webpack メモリ使用量改善

Key Points

  • 同時書き込みの上限化
  • CachedSource キャッシュ対策
  • Next.js 8 に標準搭載

Summary

Next.js 8 で導入されたビルド時のメモリ使用量最適化についての要約です。大量ページのプロジェクトで発生していた大規模な一括メモリ割当(例: 548MB)を解消するため、同時処理数の制限とアセット生成キャッシュの挙動変更により、割当を段階的に小さくし最終的に数百KB単位まで削減しました。Next.js 8 ではこれらの改善が組み込まれているため、ユーザー側での設定変更は不要です。

Key Points

  • 問題点

    • webpack の compilation.assets を非制限に同時処理していたため、各アセット生成(source()) が同時に走り大きな一括メモリ割当が発生。
    • CachedSource が source() の結果を保持しており、書き出し後に不要なキャッシュが解放されないケースがあった。
  • 解決策(実践的対処法)

    • 同時書き込み数を制限する(例: asyncLib.forEachLimit や async-sema を用いて concurrency = 15 程度に制限)。
    • webpack の出力設定 output.futureEmitAssets を有効化(または該当する webpack バージョンへアップグレード)して、不要な CachedSource キャッシュを避ける。
  • 効果

    • 大きな一括割当(約 548MB)→ 分割された約 34MB のチャンクへ削減 → 出力挙動変更で数百KB単位(約 184KB)まで低減。
    • Next.js 8 はこれらの最適化を標準で含むため、Next.js ユーザーは追加対応不要。

Practical notes for engineers

  • 自分で webpack を使っている場合は、まずビルド中のメモリプロファイルを取得して問題箇所を確認する。
  • 対策手順の例:
    • async-sema などで書き出し処理の並列度を制限する。
    • webpack を該当の改善を含むバージョンにアップデートし、必要に応じて output.futureEmitAssets: true を有効にする。
  • 大規模ページ数のプロジェクトでは並列制限と最新の webpack を組み合わせることで安定的にメモリ消費を抑えられる。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Next.js 8 の webpack メモリ改善

Next.js 8 の webpack メモリ改善

投稿日: 2019-02-19

投稿者: Connor Davis (@connordav_is), Tim Neutkens (@timneutkens)

最近、Next.js 8 がリリースされました。このリリースにはビルド時のメモリ使用量を大幅に削減する改善が含まれており、本記事ではコミュニティのために我々がどのように webpack を最適化したかを解説します。

Next.js はゼロコンフィグを目指し、webpackBabel のようなツールの上に構築されています。目的は重要なこと、つまりアプリケーションのコードに集中できるようにすることです。

モダンなウェブアプリケーションは、ホームページ、ブログ、ダッシュボード、商品一覧など、1ページまたは複数ページで構成されます。Next.js ではこれらのページがプロジェクトルートの特殊な pages ディレクトリ内のファイルになります。例えば pages/about.js は URL /about にマップされます。

フレームワーク設計上の重要な制約の1つは、単一ページから数千ページまでどちらにも適切に動作する必要があることです。

問題の発見

Serverless Next.js を実装する過程で、何百ページもあるプロジェクトで next build を実行するとメモリ使用量が高くなり、Node.js のヒープ制限(約 1.4 GB)を超えることがあることに気づきました。

ビルドプロセスのメモリ使用量を Chrome Developer Tools でプロファイリングしたところ、webpack が一度に 548 MB のメモリを確保するポイントを発見しました。確保されるメモリ量はページ数と相関しており、ページが多いほどメモリ使用量が増えていました。

プロファイルのスタックトレースを辿ることで、メモリ割り当てのスパイクを引き起こしている関数を特定しました。割り当ては source.source() メソッドが呼ばれることに由来しており、このメソッドは生成されたファイルをメモリに格納します。

さらに上の呼び出し元を見ると、compilation.assetsasyncLib.forEach で反復しており、提供された関数が compilation.assets 配列内のすべてのファイルに対して同時に呼ばれていることが分かりました。つまり、例えば 100 ページある場合、ディスクに書き出すために 100 個のファイルを同時に生成・書き出そうとしてしまっていました。

同時書き込み数の制限

この問題の解決策は、セマフォを使って同時書き込みの数を制限することです。通常は async-sema を使いますが、このケースでは webpack が既に利用している neo-asyncasyncLib.forEach に適切なメソッドがありました。

従来の同時実行(すべてのアセットで関数を同時に実行):

asyncLib.forEach(compilation.assets, (source, file, callback) => {
  // etc
});

同時実行数を制限したコード(最大 15 並列):

asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
  // etc
});

この同時実行制限を導入して再プロファイリングしたところ、メモリ割り当てが一度に 34 MB 程度ずつ小分けにされるようになりました。

キャッシュされたアセットとガベージコレクション

ただし、この変更だけでは実運用でビルドがまだメモリ不足になることがあり、さらなる調査を続けました。メモリプロファイルを詳しく見ると、source.source() を呼んだ後にメモリが解放(ガベージコレクション)されていないことが分かりました。

webpack のアセットは通常 Source クラスのインスタンスで、これらは source() メソッドを実装してファイルソースを生成します。プロファイルでは多くのアセットが CachedSource のインスタンスであることが示されていました。CachedSourcesource() が呼ばれると結果をメモリにキャッシュし、アセットが dispose されるまで保持します。

Next.js が使っている webpack プラグインを調べたところ、webpack がファイルを書き出した後に source() を呼ぶプラグインは存在せず、書き出した値をキャッシュすることに利点がないことが分かりました。

output.futureEmitAssets の導入

Tobias Koppers と協力した結果、output.futureEmitAssets という新しいオプションが実装され、 새로운アセット書き出しの挙動を opt-in できるようになりました。この新しい挙動により、時間経過で割り当てられるチャンクサイズは 182 KB 程度まで減少しました。

最終的な最適化後のプロファイラでは、時間経過で 184 KB 程度のチャンクが割り当てられているのが確認できます。

結果と影響

Next.js 8 にはこれらの最適化がすでに組み込まれています。Next.js を使用する際に何かを変更する必要はありません。この最適化は webpack 側で導入されたため、Next.js ユーザーだけでなくすべての webpack ユーザーが恩恵を受けます。

今後も Next.js と webpack のメモリ使用量とパフォーマンスの改善を継続して行っていきます。

Next.js 8 の webpack メモリ改善 | Next.js | DocsDigest