ClaudeNext.js2019/02/19 14:00

Next.js 8 Webpack Memory Improvements

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

元記事

Quick Digest

要約

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

claudejamodel: claude-sonnet-4-20250514

Next.js 8におけるWebpackメモリ使用量の大幅改善

Key Points

  • ビルド時メモリ使用量を548MBから182KBまで大幅削減
  • 並行処理制限とキャッシュ最適化による改善
  • Webpack本体への貢献でコミュニティ全体が恩恵

Summary

Next.js 8では、ビルド時のメモリ使用量を大幅に削減する最適化が実装されました。数百ページを持つプロジェクトでNode.jsのメモリ制限(約1.4GB)を超える問題を解決するため、Webpackの並行処理とキャッシュ機能を改善しました。

Key Points

  • 並行処理の制限: asyncLib.forEachからasyncLib.forEachLimitに変更し、同時書き込み数を15に制限
  • メモリ割り当ての改善: 一度に548MBの割り当てから34MBの小さなチャンクに分割
  • CachedSourceの最適化: output.futureEmitAssetsオプションにより不要なキャッシュを削減し、最終的に182KBまで縮小
  • コミュニティへの貢献: これらの最適化はWebpack本体に実装され、全てのWebpackユーザーが恩恵を受ける

Full Translation

翻訳

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

claudejamodel: claude-sonnet-4-20250514

Next.js 8 Webpack メモリ改善

Next.js 8 Webpack メモリ改善

最近、Next.js 8が導入されました。このリリースには、ビルド時のメモリ使用量の大幅な削減が含まれていました。このブログ記事では、コミュニティのためにwebpackの最適化をどのように支援したかを探ります。

Next.jsはゼロ設定で、webpackやBabelなどのツールの上に構築されています。その目標は、重要なことに集中できるよう支援することです:あなたのアプリケーションコードです。

ページベースのアーキテクチャ

現代のWebアプリケーションは1つ以上のページで構成されています。例えば、ホームページ、ブログ、ダッシュボード、または商品一覧などです。Next.jsでは、これらのページはプロジェクトのルートにある特別なpagesディレクトリ内のファイルになります。例えば:pages/about.jsファイルは/aboutのURLにマップされます。

フレームワークの主要な設計制約の1つは、単一ページと数千ページの両方で適切に動作する必要があることです。

メモリ使用量の問題の発見

Serverless Next.jsの実装中に、数百ページのプロジェクトでnext buildを実行すると高いメモリ使用量が発生することが明らかになりました。時にはNode.jsが持つ約1.4 GBのメモリヒープ制限を超えることもありました。

Chrome開発者ツールを使用してビルドプロセスのメモリ使用量をプロファイリングし始めました。結果のプロファイルで、webpackが一度に548 MBのメモリチャンクを割り当てるポイントを発見しました。割り当てられるメモリ量はページ数と直接相関しており、ページが多いほどメモリ使用量が増加することを意味していました。

根本原因の特定

メモリプロファイルのスタックトレースを調べることで、メモリ割り当てスパイクを引き起こした関数を特定することができました。割り当て自体はsource.source()メソッドの呼び出しから発生し、これが結果ファイルを生成してメモリに保存していました。

しかし、source()メソッドを呼び出す関数をさらに上位で見ると、compilation.assetsasyncLib.forEachを使用して反復処理されていることがわかりました。これは、提供された関数がcompilation.assets配列内のすべてのファイルに対して同時に呼び出されることを意味していました。

つまり、例えば100ページがあり、各ページをディスクに書き込む必要がある場合、上記のコードは100すべてを同時に書き込もうとし、100ファイルすべてを同時に生成することになります。

解決策:同時実行制限

この問題の解決策は、セマフォを使用して同時書き込み数を制限することです。一般的にはasync-semaを使用しますが、この場合webpackにはneo-asyncで利用可能な適切なメソッドがすでにありました:

// すべてのアセットに対して関数を同時実行する以前のコード
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のインスタンスであることが示されました。

CachedSourceの動作方法は、source()が呼び出されると、アセットが破棄されるまで結果がメモリ内にキャッシュされることです。Next.jsが使用するwebpackプラグインを調べると、webpackがファイルを書き込んだ後にsource()を呼び出すプラグインがないことがわかり、書き込まれた値をキャッシュすることに利益がないことを意味していました。

最終的な解決策

Tobias Koppersとの協力の後、彼は新しいアセット書き込み動作にオプトインできる新しいオプションoutput.futureEmitAssetsを実装しました。この新しい動作により、割り当てられるチャンクは時間の経過とともに182 KBまで削減されました。

結論

Next.js 8にはすでにこれらすべての最適化が組み込まれています。Next.jsを使用する際に何かを変更する必要はありません。

この最適化はwebpackに導入されたため、Next.jsユーザーだけでなく、すべてのwebpackユーザーがこれらの最適化の恩恵を受けることができます。

私たちはNext.jsとwebpackのメモリ使用量とパフォーマンスの改善を積極的に続けていきます。