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.assetsがasyncLib.forEachを使用して反復処理されていることがわかりました。これは、提供された関数がcompilation.assets配列内のすべてのファイルに対して同時に呼び出されることを意味していました。
つまり、例えば100ページがあり、各ページをディスクに書き込む必要がある場合、上記のコードは100すべてを同時に書き込もうとし、100ファイルすべてを同時に生成することになります。
解決策:同時実行制限
この問題の解決策は、セマフォを使用して同時書き込み数を制限することです。一般的にはasync-semaを使用しますが、この場合webpackにはneo-asyncで利用可能な適切なメソッドがすでにありました:
asyncLib.forEach(compilation.assets, (source, file, callback) => {
});
asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
});
さらなる最適化
この同時実行制限を実装し、ビルドメモリ使用量を再度プロファイリングした後、メモリ割り当てが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のメモリ使用量とパフォーマンスの改善を積極的に続けていきます。