trace-to-treeでNext.jsの起動タイムをトレースする

開発環境で作ったNext13のプロジェクトが、どうも動きが遅い（ページ遷移など）ので、原因を調査してみることにしました。

それで見つけたのがzenn様による以下のページです。

https://zenn.dev/mkt/articles/543669021d9a1e

ですが、この記事にしれっと出てくる**trace-to-tree.mjs**という謎のライブラリ、これについて入手方法、使用方法が書かれていません。WEBを検索してみても日本語のサイトはほとんど出てこず、有効な手がかりを得たのがDEV COMMUNITY様のこの記事です。

https://dev.to/hansottowirtz/analyzing-nexttrace-3m1m

なるほど、githubから必要なライブラリを入手すればいいのだと思い、実践してみました。

# 今回の使用環境
以下の通りです

- Rocky Linux 9.1
- Next13.4
- NPM10.2.4
- node20.11.0

また今回、トレースしたプロジェクトは**next13**という少し紛らわしい名前になっています。

# trace-to-tree.mjsを入手する
まずはgithubからNext.jsのライブラリをクローンしてしまいましょう（プロジェクト名とは別です）

```
# cd /var/www/html
# git clone https://github.com/vercel/next.js.git && cd next.js
```

## 使い方
上述のDEVのページを見る限り、

```
#node scripts/trace-to-tree.mjs /your/app/.next/trace
```

となっているので、

```
#node 使用元のtrace-to.tree.mjsのパス トレースしたいプロジェクトのtraceファイル
```

このようにパスを変えれば、どこに置いても、どれでも対応できるみたいです。なので自分のプロジェクトのnext13に移行しておきました（クローン後のnext.jsは削除しても問題ありません）

```
#cp next.js/scripts/trace-to-tree.mjs /next13
```

# 起動までの道のり
では、実行してみます。traceの場所は.next/traceとなっているので、以下のように記述します。

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

すると

:::note alert
cannot find module 'event-stream'
:::

こんなメッセージが出たので調べてみると、npmを入れ直した方がいいという意見が大半。ところがこれを実施しても解決せず。それでtrace-to-treeのmjsファイルを覗いてみるとevent-streamというライブラリをインポートしているような記述があります。

```js:trace-to-tree.mjs
import fs from 'fs'
import eventStream from 'event-stream'
/*後略*/
```

だとすると、これはライブラリが足りないだけなのでは？　そう思ってインストールしてみたら、普通にインストールされました。

```
#npm i event-stream
```

晴れて再度起動してみると、今度は

:::note alert
cannot open file or directory ../packages/next/dist/lib/picocolors.js
:::

このような文言が表示されていました。どうもpicocolors.jsというライブラリは外部から呼び出しているようなので、それならばとこれもパッケージをインストールしてみました。ちなみにpicocolorsとはメッセージを蛍光色に変える働きを持ちます（参考の元記事の画像のように、カラフルになります）。

```
#npm i picocolors
```

それからmjsファイルを以下のように編集しておきます。

```js:trace-to-tree.mjs
//インポート先を外部パスからライブラリへ
import {
  bold,
  blue,
  cyan,
  green,
  magenta,
  red,
  yellow,
} from 'picocolors'
```

すると、今度はblueが読めないというエラーが出ました。ここでかなり試行錯誤したのですが、解決の決め手はpicocolorsの公式サイトでした。

https://www.npmjs.com/package/picocolors

そこに使用方法がありましたが、このようにあります。

```
import pc from "picocolors"
```

つまり、**オブジェクトからカラープロパティを呼び出す**仕様のようでした（ローカルのpicocolors.jsとは別物なのかも知れません）。なので、この仕様にしたがい、trace-to-tree.mjsを再編集（色指定のメソッドにpcというオブジェクトを付与していく。blueならpc.blue）していきます。

59行から163行までの、色分けの部分を以下のように書き換えました。

```js:trace-to-tree.mjs
/*import {
  bold,
  blue,
  cyan,
  green,
  magenta,
  red,
  yellow,
} from 'picocolors'*/
import pc from 'picocolors';
/*中略*/
const formatDuration = (duration, isBold) => {
  const color = isBold ? pc.black : (x) => x
  if (duration < 1000) {
    return color(`${duration} µs`)
  } else if (duration < 10000) {
    return color(`${Math.round(duration / 100) / 10} ms`)
  } else if (duration < 100000) {
    return color(`${Math.round(duration / 1000)} ms`)
  } else if (duration < 1_000_000) {
    return color(pc.cyan(`${Math.round(duration / 1000)} ms`))
  } else if (duration < 10_000_000) {
    return color(pc.green(`${Math.round(duration / 100000) / 10}s`))
  } else if (duration < 20_000_000) {
    return color(pc.yellow(`${Math.round(duration / 1000000)}s`))
  } else if (duration < 100_000_000) {
    return color(pc.red(`${Math.round(duration / 1000000)}s`))
  } else {
    return color('🔥' + red(`${Math.round(duration / 1000000)}s`))
  }
}

const formatTimes = (event) => {
  const range = event.range - event.timestamp
  const additionalInfo = []
  if (event.total && event.total !== range)
    additionalInfo.push(`total ${formatDuration(event.total)}`)
  if (event.duration !== range)
    additionalInfo.push(`self ${formatDuration(event.duration, pc.black)}`)
  return `${formatDuration(range, additionalInfo.length === 0)}${
    additionalInfo.length ? ` (${additionalInfo.join(', ')})` : ''
  }`
}

const formatFilename = (filename) => {
  return cleanFilename(filename).replace(/.+[\\/]node_modules[\\/]/, '')
}

const cleanFilename = (filename) => {
  if (filename.includes('&absolutePagePath=')) {
    filename =
      'page ' +
      decodeURIComponent(
        filename.replace(/.+&absolutePagePath=/, '').slice(0, -1)
      )
  }
  filename = filename.replace(/.+!(?!$)/, '')
  return filename
}

const getPackageName = (filename) => {
  const match = /.+[\\/]node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
    cleanFilename(filename)
  )
  return match && match[1]
}

const formatEvent = (event) => {
  let head
  switch (event.name) {
    case 'webpack-compilation':
      head = `${pc.bold(`${event.tags.name} compilation`)} ${formatTimes(event)}`
      break
    case 'webpack-invalidated-client':
    case 'webpack-invalidated-server':
      head = `${pc.bold(`${event.name.slice(-6)} recompilation`)} ${
        event.tags.trigger === 'manual'
          ? '(new page discovered)'
          : `(${formatFilename(event.tags.trigger)})`
      } ${formatTimes(event)}`
      break
    case 'add-entry':
      head = `${pc.blue('entry')} ${formatFilename(event.tags.request)}`
      break
    case 'hot-reloader':
      head = `${pc.bold(pc.green(`hot reloader`))}`
      break
    case 'export-page':
      head = `${event.name} ${event.tags.path}  ${formatTimes(event)}`
      break
    default:
      if (event.name.startsWith('build-module-')) {
        const { mergedChildren, childrenTimings, packageName } = event
        head = `${pc.magenta('module')} ${
          packageName
            ? `${pc.bold(pc.cyan(packageName))} (${formatFilename(event.tags.name)}${
                mergedChildren ? ` + ${mergedChildren}` : ''
              })`
            : formatFilename(event.tags.name)
        } ${formatTimes(event)}`
        if (childrenTimings && Object.keys(childrenTimings).length) {
          head += ` [${Object.keys(childrenTimings)
            .map((key) => `${key} ${formatDuration(childrenTimings[key])}`)
            .join(', ')}]`
        }
      } else {
        head = `${event.name} ${formatTimes(event)}`
      }
      break
  }
  if (event.children && event.children.length) {
    return head + '\n' + treeChildren(event.children.map(formatEvent))
  } else {
    return head
  }
}
```

これでやっとエラーなく起動することができました。

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

# トレースの結果
どうもmagic-uiというものの起動に手間取っているようです。ですが、それについて調べてみても根本的なTurbopackの起動にかかわるライブラリなので、修正不可だということでした。ただ、気になったのはどうも使用しているNextのバージョンが悪い（バグを持っている？）のかと思い、package.jsonのnextを13.4から13.5に書き換えてみました。

```
#remove -rf node_modules
#rm package-lock.json
#npm i
```

これでNextのバージョンを刷新すると、かなり起動速度が上がりました（magic-uiのレスポンス速度は変わらなかったですが）。他のライブラリも干渉していたのかも知れません。

そこで気になったライブラリとして**Chakra UI**ですが、これについてよく調べてみると

https://github.com/chakra-ui/chakra-ui/issues/7649

**Next13からはクライアントを使う必要がない**と書かれています（下の方に高評価の意見があります）。しかも13.4までは起動に干渉していたらしく13.5からはその問題が改善されていたようですが、クライアント宣言をコメントアウトすると、更に高速化を実現できました。

https://zenn.dev/osayubot/articles/1fbbb5415e3a5d

# ビルドでも試してみる
開発環境の起動をトレースできるなら、ビルドもトレースできると思い、試してみました。

準備として、ビルドできるようにnext.config.jsを書き換えます。Next13.5でビルドする場合はまず、以下のように下準備が必要です。

```next.config.js
module.exports = {
    output: "export",
    trailingSlash: true,
}
```

あとはコマンドを実行するだけです。outというフォルダが作成されれば、ビルドは成功しています。
```
# npm run build
```

もう一度、さっきのコマンドを実行すると、先ほどと別の結果が表示されています。