2019年2月19日(火)
Next.js 8 Webpack メモリ使用量の改善
投稿者最近、Next.js 8 がリリースされました。このリリースでは、ビルド時のメモリ使用量が大幅に削減されました。この記事では、コミュニティのために webpack をどのように最適化したかを探ります。
Next.js はゼロコンフィギュレーションで、webpack や Babel といったツールの上に構築されており、その目的は、開発者が重要なアプリケーションコードに集中できるようにすることです。
モダンなウェブアプリケーションは、1つ以上のページで構成されています。例えば、ホームページ、ブログ、ダッシュボード、商品一覧などです。
Next.js では、これらのページはプロジェクトのルートにある特別な pages ディレクトリ内のファイルになります。
例えば、ファイル pages/about.js は URL /about にマッピングされます。
このフレームワークの主要な設計制約の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) => {
// etc
});以前のコード:すべての資産に対して関数を並列実行していました。
asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
// etc
});新しいコード:一度に最大 15 個まで関数を並列実行します。
この並列性制限を実装し、ビルドメモリ使用量を再度プロファイリングしたところ、メモリ割り当てが **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 のメモリ使用量とパフォーマンスは、今後も積極的に改善していきます。