STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

ビルドプロセスを見直して Next.js 製アプリケーションのビルド時間を 10 分短縮

この記事は STORES Advent Calendar 2025 の 19 日目の記事です。

はじめに

こんにちは、 id:sushichan044 です。

この記事では、直近で取り組んだ Next.js 製アプリケーションのビルドの高速化を振り返ります。

対象となったのは STORES 予約を利用する事業者向けのアプリケーションで、2021 年 2 月から開発されています。

stores.fun

$ git log --all --reverse
commit <Hidden>
Author: <Hidden>
Date:   Thu Feb 25 16:37:51 2021 +0900

    Initial commit from Create Next App

前提

今回扱うアプリケーションは、以下のような構成になっています。

  • Node.js v22.18.0
  • next@14.2.35
    • Pages Router
  • react@18.3.1
  • react-dom@18.3.1

なお、本記事では Babel や SWC、Webpack といったビルドプロセスを構成するツール群がビルド時間にどう影響していたかを扱いますが、それぞれのしくみや設定を詳しく解説することは目的としていません。

背景と課題

機能追加に伴ってページ数や依存関係が増えていくにつれアプリケーションのビルド時間も徐々に長くなっていき、2025 年 11 月頃には以下のような状態になっていました。

  • 開発サーバー起動後、最初の page compile に約 5 分
  • デプロイ時の next build の実行に約 15 分

開発体制の変化により複数のフロントエンドを並行して触るようになったことで、このアプリケーションのビルド時間が他の Next.js 製アプリケーションと比べて明らかに長いことを意識するようになりました。

product.st.inc

ビルド時間が長いと、

  • ローカル開発のテンポが落ちる
  • デプロイや検証に時間がかかる
  • 障害対応時に修正が本番環境へ反映されるまでの時間が伸びる

といった問題が発生します。

これは単なる開発体験の問題ではなく、プロダクトの信頼性にも影響するため、腰を据えて改善に取り組むことにしました。

調査

build trace の分析

まずはボトルネックを特定するために、Next.js の build trace を分析しました。

Next.js では、ビルド中に実行された処理が .next/trace に記録されます。 この trace には、Webpack によるバンドル処理や Babel / SWC によるトランスパイル処理、各ページのビルド処理などについて、 それぞれがどのくらいの時間実行されていたかが記録されています。

ただし、このファイルをそのまま読むのは現実的ではありません。
そこで、Next.js の公式リポジトリにあるスクリプトを使って trace を Trace Event Format に変換しました。

github.com

この形式に変換することで、https://ui.perfetto.dev 1 などの Web UI 上で可視化できます。

trace のグラフが表示されている。next-build, run-webpack-compiler, webpack-compilation, add-entry などの event が見える。
可視化した trace。perfetto は SQL でデータを集計できたりするので便利

可視化した trace と元の .next/trace を突き合わせて分析した結果、 i18n(国際化)2 関連の処理を多く含むコンポーネントほど、ビルド時間が長いという傾向が見えてきました。

ここからはリポジトリ内の i18n 関連の処理を深堀りして原因を探っていきます。

原因の特定

この Next.js アプリケーションでは TypeScript / React / JSX を JavaScript に変換するトランスパイラとして Babel を利用していました。 Babel は plugin system を持っており、Babel 本体がプログラムから生成した AST(抽象構文木)に対して走査・書き換えを行う処理を plugin として実装できます。

babeljs.io

i18n 周りの処理を調査したところ、特定の Babel plugin が、

  • AST を走査して i18n 用ライブラリの API の呼び出しを探し
  • 一定の規則に従ってコード変換を行う

という処理を行っていることが分かりました。

この plugin は i18n 関連のボイラープレートを減らす目的で導入されており、 AST 上の広い範囲を対象とした走査と変換を必要としていました。 その結果、ファイル数やコード量の増加に伴って実行コストが増えやすい状態になっていました。

試しにこの plugin を無効化してビルドしたところ、ビルド時間がおおよそ半分に短縮されたため、 この Babel plugin が主要なボトルネックであると判断しました。

ボトルネックの解消

方針の整理

Babel の設定を改めて整理すると、利用されている plugin は大きく次の 2 種類に分類できました。

  1. トランスパイルに必ず必要なもの
    • TypeScript / JSX を JavaScript にトランスパイルするための plugin 群
      • Next.js が next/babel preset を通して自動で設定する
  2. 一定の規則に従って AST を機械的に変換し、開発体験を向上させるもの
    • 今回ボトルネックになっていた i18n 関連の plugin

このうち 2. に該当する plugin は、ビルドプロセスに必須ではありません。
同様のチェックや変換は、ESLint やスクリプトなどを使ってビルドとは別のタイミングで実行できます。

そこで、「重い AST 変換はビルド時に行わず、事前にソースコードへ適用する」という方針を取りました。

採用しなかったアプローチ

Babel plugin の高速化

方針を検討する過程で、ボトルネックとなっている Babel plugin 自体を改修し AST の走査や変換処理を高速化する案も検討しました。 この plugin は OSS なので、改善案を検討して upstream に貢献するという選択肢も十分に考えられるものでした。

一方で、2025 年 12 月現在、Next.js では SWC がデフォルトのトランスパイラとなっています。 将来的に SWC を前提としたビルド構成へ移行していくことを考えると、ビルド時に Babel に強く依存した処理を残すことは、移行のブロッカーになり得ます。

そのため今回は、Babel plugin の高速化ではなく、ビルドプロセスから Babel による i18n 関連のコード変換そのものを外すという判断を取りました。

解決アプローチの実装

この方針を踏まえ、既存の Babel plugin を流用しつつ、ビルド時ではなく開発段階でコード変換を行う方法を検討しました。

具体的な実装方法を調べる中で、id:mizdra さんがビルド時以外に Babel を実行する事例を紹介している記事を見つけました。

www.mizdra.net

この事例を参考に、該当の Babel plugin によるコード変換を開発段階でソースコードへ直接適用するスクリプトとして実装しました。

このスクリプトを GitHub Actions 上の CI でも実行し、コードに差分が出ないかを検証することで適用漏れを防いでいます。

import { readFile, writeFile } from 'node:fs/promises'
import { transformFromAstAsync } from '@babel/core'
import { parse as babelParse } from '@babel/parser'
import { parse, print } from 'recast'
import { glob } from 'tinyglobby'

async function codemod(filePath: string) {
  const input = await readFile(filePath, 'utf8')

  const ast = parse(input, {
    parser: {
      parse(source: string) {
        return babelParse(source, {
          sourceFilename: filePath,
          tokens: true,
          sourceType: 'module',
          plugins: ['typescript', 'classProperties', 'jsx'],
        })
      },
    },
  })

  // mutate `ast` directly
  await transformFromAstAsync(ast, input, {
    code: false,
    // babel の設定ファイルを書き換えて、
    // この envName が指定された場合は i18n 関連の plugin のみを適用するようにしておく
    // https://babeljs.io/docs/config-files#apienv
    envName: 'i18n-codemod',
    filename: filePath,
    cloneInputAst: false,
  })

  const modifiedCode = print(ast).code
  await writeFile(filePath, modifiedCode, 'utf8')
}

const main = async () => {
  const files = await glob('src/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}')
  console.log(`Found ${files.length} files to process. Processing...`)

  await Promise.all(
    files.map(async (filePath) => {
      await codemod(filePath)
    })
  )

  console.log(`Successfully processed ${files.length} files.`)
}

main().catch(console.error)

実装上の補足

Babel の API を素朴に使って AST を操作すると、改行やコメントなどのフォーマット情報が失われ、変換後のコードが大きく崩れてしまいます。

ビルドプロセスの中で実行される Babel のコード変換であれば、最終的に生成されるのはデプロイ用の JavaScript であり、フォーマット上の差分は問題になりません。

一方で、今回のようにコード変換をソースコードへ直接適用する場合、フォーマットの崩れはレビューのしづらさや不要な差分の増加につながります。

そこで今回は recast を利用し、whitespace・コメント・改行といったフォーマット情報を可能な限り保持したまま AST 変換を行いました。

github.com

高速化の成果と今後の展望

i18n 関連の重いコード変換をビルドプロセスから切り離したことで、ビルド時間がおおよそ半分に短縮されました。

さらに、Babel に強く依存した処理がなくなったことで、より高速なトランスパイラである SWC を有効化できるようになりました。

swc.rs

SWC も有効化して計測した結果、最終的にビルド時間は次のようになりました。

  • デプロイ時の next build: 15 分 → 3 分 40 秒
  • 開発サーバー起動後の最初の page compile: 5 分 → 10 秒

特にデプロイにかかる時間が大きく短縮されたことで、検証や障害対応を含めた運用のテンポが改善しました。

今後は、コード変換処理を ESLint plugin などに移植することで、より自然に開発プロセスに組み込んでいく予定です。

まとめ

今回の改善では、Next.js の build trace を分析することでビルドプロセスのボトルネックを特定し、ビルドに必須ではない重い処理を事前に実行するように見直しました。 その結果ビルドプロセス全体を整理でき、ビルド時間を大幅に短縮できました。

各処理が本当にビルド時に実行されるべきものかどうかを考えることで、小さな変更でも改善につながることがあります。

今後も、今回のような視点を活かしながらビルド周りの改善に取り組んでいきたいと思います。


STORES では開発環境やビルドの高速化に情熱を持ったエンジニアを募集しています。

jobs.st.inc


  1. 昔 Google Chrome の chrome://tracing で利用できた Trace Viewer と同じものです。
  2. STORES 予約の事業者向けダッシュボードは英語での表示にも対応しています