コンテンツへスキップ
ブログに戻る

2019年2月19日 火曜日

Next.js 8 Webpack メモリ改善

投稿者

最近、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開発者ツールを使用して、ビルドプロセスのメモリ使用量をプロファイリングし始めました。

その結果のプロファイルで、webpackが一度に548 MBのメモリを割り当てるポイントを発見しました。

割り当てられるメモリ量はページ数に直接相関しており、ページ数が増えるほどメモリ使用量も増えることを意味していました。

The Chrome Developer Tools memory profiler showed 548 MB being allocated at once
Chrome開発者ツールのメモリプロファイラーは、548 MBが一度に割り当てられていることを示しました。

メモリプロファイルのスタックトレースを追跡することで、メモリ割り当ての急増を引き起こした関数を特定することができました。

割り当て自体は、結果のファイルを生成してメモリに保存するsource.source()メソッドの呼び出しから来ていました。

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

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

この問題の解決策は、同時書き込みの数を制限するためにセマフォを使用することです。通常、これにはasync-semaを使用しますが、この場合はwebpackが既にneo-asyncに適切なメソッドを用意していました。

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

すべてのアセットに対して関数を同時に実行していた以前のコード

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

一度に最大15個まで同時に関数を実行する新しいコード

この同時実行制限を実装し、再度ビルドのメモリ使用量をプロファイリングした後、メモリ割り当てが34 MBのより小さなチャンクに分割されていることが確認できました。

The profiler now showed chunks of 34 MB being allocated over time
プロファイラーは、時間とともに34 MBのチャンクが割り当てられていることを示しました。

この変更は非常に有望な結果を示しましたが、実際にはビルドが依然としてメモリ不足に陥ったため、私たちはプロファイリングと問題の調査を続けました。

メモリプロファイルをさらに調査したところ、source.source()メソッドが呼び出された後もメモリがクリーンアップ(ガーベージコレクション)されていないことに気づきました。

webpackでは、アセットは通常、Sourceクラスのインスタンスです。これらのクラスはすべて、ファイルソースを生成するsource()メソッドを実装しています。

プロファイルは、多くのアセットがCachedSourceのインスタンスであることを示しました。CachedSourceの動作は、source()が呼び出されると、アセットが破棄されるまで結果がメモリにキャッシュされるというものです。

Next.jsが使用するwebpackプラグインを検査した結果、webpackがファイルを書き込んだ後にsource()を呼び出すプラグインがないことがわかりました。これは、書き込まれた値をキャッシュするメリットがないことを意味します。

Tobias Koppers協力した後、彼は新しいアセット書き込み動作を選択できるようにするoutput.futureEmitAssetsという新しいオプションを実装しました。

この新しい動作により、割り当てられるチャンクは時間とともに182 KBに削減されました。

After all optimizations the profiler shows chunks of 184 KB being allocated over time
すべての最適化後、プロファイラーは時間とともに184 KBのチャンクが割り当てられていることを示しています。

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

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

私たちは引き続き、Next.jsとwebpackのメモリ使用量とパフォーマンスの改善に積極的に取り組んでいきます。