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

2019年2月19日火曜日

Next.js 8 Webpack のメモリ改善

投稿者

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

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

現代の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 のメモリを割り当てるポイントを発見しました。

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

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 ページすべてを同時に書き込もうとし、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のメモリ使用量とパフォーマンスを積極的に改善していきます。