🚄

Next.jsのビルド速度を改善したい〜Next.jsのTrace情報を分析してボトルネックとなっている処理を特定してみる

に公開

こんにちは!アルダグラムでエンジニアをしている今町です。

ある日、Next.js で作られた Web アプリケーションをデプロイしていたときの話。
「なんか、Next.js のビルド速度、遅くない…?」
ということに気づきました(全体のデプロイ時間のうち、7 割がビルド時間になっていました)

そんなわけで、Next.js のビルド速度を改善することにしました。

Next.js のビルド時間が遅い原因(ボトルネック)の調査

まずは、Next.js のビルド時間のうち、どこの処理がボトルネックになっているのか特定する必要があります。Next.js は .next/trace にトレース情報を自動で出力してくれます。このトレース情報を分析することで、ビルド時の処理毎の処理時間を割り出すことができます。

Next.js のビルドにおけるトレース情報の用意

ローカル環境で Next.js のビルドを実行してみて、ビルド処理における Next.js のトレース情報を取得してみましょう(.next/trace に trace 情報が出力されます)

npm run build

.next/trace に出力されたデータを見てみると、以下のような JSON で記述されたデータになっています。正直、人間が見てわかるデータになっていないですね…。見にくいデータなので、データ加工を行う必要があります。

.next/trace
// 見にくいデータ
[{"name":"generate-buildid","duration":96,"timestamp":356037304708,"id":4,"parentId":1,"tags":{},"startTime":1748005352098,"traceId":"6783aff358590a83"},{"name":"load-custom-routes","duration":127,"timestamp":356037304839,"id":5,"parentId":1,"tags":{},"startTime":1748005352098,"traceId":"6783aff358590a83"},...]

Next.js の GitHub リポジトリに、このトレース情報を tree 構造のデータ(人間が見れるデータ)に変換するスクリプトがあるので、それを活用します。
https://github.com/vercel/next.js/blob/canary/scripts/trace-to-tree.mjs

必要なパッケージを手元にインストールします。

npm install picocolors -D
npm install event-stream -D

元のソースコードのままだと手元で動かないので、一部ソースコードを修正します(picocolors のインポート方法を修正。なぜか、next.js のリポジトリ内にある picocolors.js ファイルを参照するカタチになっているので、その部分を修正します)

trace-to-tree.mjs
- import {
-   bold,
-   blue,
-   cyan,
-   green,
-   magenta,
-   red,
-   yellow,
- } from '../packages/next/dist/lib/picocolors.js'
+ import pkg from "picocolors";
+
+ const { bold, blue, cyan, green, magenta, red, yellow } = pkg;

スクリプトを用意できたら、以下のコマンドを実行します。

node trace-to-tree.mjs .next/trace

以下のように trace ツリーが標準出力されるので、ファイルに保存しておきましょう。

image.png

AI にトレース情報を解釈させる(オプション)

tree データの状態でも十分、人間が読めるカタチになっていますが、どこが処理上のボトルネックになっているのか AI に整理してもらうと楽です。
実際に AI にデータを読み込ませると、

  1. 型チェック
  2. Lint チェック
  3. バンドル処理(Webpack コンパイル)

の3つが処理上のボトルネックであることがわかりました(今回扱った Next.js アプリケーションでの話です)
※ Next.js のビルド(npm run build)では、型チェックと Lint チェックが自動で実行されるようになっている。

Next.js のビルドパフォーマンスの改善

それでは、ここからはトレース情報の分析に基づいて、Next.js のビルドパフォーマンスを改善してみましょう。
以下はあくまで具体例なので、Next.js アプリケーションによって判断してください。

1. 型チェック

CI で型チェックは実施済みのため、検証環境へのデプロイ時に限り、型チェックを Skip するようにしました(一方で、本番環境へのデプロイでは、型チェックをスキップしないようにしました)
next.config.ts の設定ファイルに typescript.ignoreBuildErrors: true の設定を追加することで、型チェックをスキップできます。
https://nextjs-ja-translation-docs.vercel.app/docs/api-reference/next.config.js/ignoring-typescript-errors

以下は、設定例になります。

next.config.ts
module.exports = {
  typescript: {
    // 本番環境へのデプロイ以外では、型チェックをスキップする
    ignoreBuildErrors: process.env.NODE_ENV !== "production",
  },
  // その他の設定
};

2. Lint チェック

CI で Lint チェックは実施済みのため、Lint チェックを Skip するようにしました。
next.config.ts の設定ファイルに eslint.ignoreDuringBuilds: true の設定を追加することで、Lint チェックをスキップできます。
https://nextjs.org/docs/app/api-reference/config/next-config-js/eslint

next.config.ts
module.exports = {
  eslint: {
    ignoreDuringBuilds: true,
  },
  // その他の設定
};

3-a. バンドル処理(Rust 製のバンドラーに置き換える)

結論、Vercel が開発中の Turbopack が next build に完全対応するまで待つことにしました(背景は以下)

今回対象である Next.js のコードのバンドルには Webpack を使っていました。なので、Turbopack や Rspack のような Rust 製のバンドラーに変更することで速度改善が見込める可能性があります。

Rspack は Webpack の互換性を意識した後継バンドラーなんですが、Next.js との integration はまだ experimental のステータスとなっており、本番投入するには二の足を踏んでしまいます。

一方で、Vercel が開発中の Turbopack ですが、Next.js 15.4 でようやく next build に対する α 版がリリースされました。Next.js 16 で β 版に到達する想定となっています。こちらも、本番投入するには時期尚早といった状況です。

個人的な主観としては、Next.js の開発元である Vercel が開発中の Turbopack を選択したほうが良さそうだと思っています。思っていますが、Turbopack は Webpack との互換性がないのが懸念点としてあります(マイグレーションで苦労しそう…)

3-b. バンドル処理(各種パッケージの見直し)

Next.js の Bundle Analyzer プラグインを導入することで、バンドルの解析ができます。

例えば、next.config.ts のように設定を書きます。

next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'
import type { NextConfig } from 'next'

const bundleAnalyzerConfig = {
    enabled: process.env.ANALYZE === 'true',
}

const nextConfig: NextConfig = {
  // 既存の設定
}

module.exports = withBundleAnalyzer(bundleAnalyzerConfig)(nextConfig)

そのうえで、以下のようにコマンドを実行すると、バンドル情報が Web ブラウザ上で開きます。

ANALYZE=true npm run build

バンドルサイズを確認できたら、サイズの大きいものから順に最適化を検討します。また、トレース情報と対比しながら、バンドルに時間がかかっている箇所を特定するのもよいです。こちらも、扱っているパッケージに依存するため、詳細は割愛します。

ビルドパフォーマンス結果

上記のもろもろの改善を行った結果、ビルド時間を以前よりも 4.5 分短縮できました 🎉

Before After
12.2 分(729s) 7.5 分(447s)

image.png

(補足)GitHub Actions & Docker コンテナ上で Next.js のビルドを行っている場合

Docker Buildx の利用

Docker コンテナ上で Next.js のビルドを実行している場合、キャッシュが効いていないと処理が遅くなる要因となります。
そこで、docker buildx を使い、キャッシュの保存先を指定することで、繰り返し Next.js のビルドをしたときにキャッシュを効かせて、ビルド時間を短縮することができます。

GitHub Actions を利用している場合、Buildx をセットアップするカスタム action が提供されているので、これを利用します。

また、Docker コマンドの具体例は、以下のとおりです。

docker buildx build \
  --platform linux/arm64 \
  --cache-from type=registry,ref=<registry>/<cache-image>[,parameters...] \
  --cache-to type=registry,ref=<registry>/<cache-image>[,parameters...] \
  --push \
  -t <registry>/<image> \
  -f dockerfile \
  .
  • --cache-from オプションでイメージレジストリから現在のビルドにキャッシュをインポートするように指定します。
  • --cache-to オプションでキャッシュ保存先であるイメージレジストリを指定します。

使っているサービス(ECR 等)で指定内容が変わってくるので、詳細は割愛します。

最後に

型チェック・Lint チェックのスキップは、本質的な問題解決というわけではないですが、Rust 製の Linter を使っていないと、Lint チェックの処理時間も無視できないレベルで時間がかかることがあります。このあたりの処理のスキップは、割とビルド時間の短縮に効果的です。
また、Docker コンテナ上でビルドしている場合は、Docker Buildx でのキャッシュ指定はビルド時間短縮に効果的なので、設定しておきたいところです。

地味な小技が多いのですが、この記事が Next.js のビルド時間が長くて困っている人の助けになったら幸いです。

アルダグラム Tech Blog

Discussion