2019年2月19日火曜日
Next.js 8 Webpack のメモリ改善
投稿者最近、Next.js 8 がリリースされました。このリリースには、ビルド時のメモリ使用量を大幅に削減する機能が含まれています。このブログ記事では、コミュニティのために webpack をどのように最適化したかについて説明します。
Next.js はゼロ設定で、webpack や Babel などのツールの上に構築されています。その目的は、アプリケーションコードという重要なことに集中できるようにすることです。
現代のWebアプリケーションは、1つ以上のページで構成されています。たとえば、ホームページ、ブログ、ダッシュボード、商品リストなどです。
Next.js では、これらのページはプロジェクトのルートにある特別な pages
ディレクトリ内のファイルになります。
たとえば、ファイル pages/about.js
は URL /about
にマッピングされます。
フレームワークの重要な設計制約の1つは、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のメモリ使用量とパフォーマンスを積極的に改善していきます。