開発環境で作ったNext13のプロジェクトが、どうも動きが遅い(ページ遷移など)ので、原因を調査してみることにしました。
それで見つけたのがzenn様による以下のページです。
ですが、この記事にしれっと出てくるtrace-to-tree.mjsという謎のライブラリ、これについて入手方法、使用方法が書かれていません。WEBを検索してみても日本語のサイトはほとんど出てこず、有効な手がかりを得たのがDEV COMMUNITY様のこの記事です。
なるほど、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
すると
cannot find module 'event-stream'
こんなメッセージが出たので調べてみると、npmを入れ直した方がいいという意見が大半。ところがこれを実施しても解決せず。それでtrace-to-treeのmjsファイルを覗いてみるとevent-streamというライブラリをインポートしているような記述があります。
import fs from 'fs'
import eventStream from 'event-stream'
/*後略*/
だとすると、これはライブラリが足りないだけなのでは? そう思ってインストールしてみたら、普通にインストールされました。
#npm i event-stream
晴れて再度起動してみると、今度は
cannot open file or directory ../packages/next/dist/lib/picocolors.js
このような文言が表示されていました。どうもpicocolors.jsというライブラリは外部から呼び出しているようなので、それならばとこれもパッケージをインストールしてみました。ちなみにpicocolorsとはメッセージを蛍光色に変える働きを持ちます(参考の元記事の画像のように、カラフルになります)。
#npm i picocolors
それからmjsファイルを以下のように編集しておきます。
//インポート先を外部パスからライブラリへ
import {
bold,
blue,
cyan,
green,
magenta,
red,
yellow,
} from 'picocolors'
すると、今度はblueが読めないというエラーが出ました。ここでかなり試行錯誤したのですが、解決の決め手はpicocolorsの公式サイトでした。
そこに使用方法がありましたが、このようにあります。
import pc from "picocolors"
つまり、オブジェクトからカラープロパティを呼び出す仕様のようでした(ローカルのpicocolors.jsとは別物なのかも知れません)。なので、この仕様にしたがい、trace-to-tree.mjsを再編集(色指定のメソッドにpcというオブジェクトを付与していく。blueならpc.blue)していきます。
59ページから163ページまでの、色分けの部分を以下のように書き換えました。
/*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ですが、これについてよく調べてみると
Next13からはクライアントを使う必要がないと書かれています(下の方に高評価の意見があります)。しかも13.4までは起動に干渉していたらしく13.5からはその問題が改善されていたようですが、クライアント宣言をコメントアウトすると、更に高速化を実現できました。
ビルドでも試してみる
開発環境の起動をトレースできるなら、ビルドもトレースできると思い、試してみました。
準備として、ビルドできるようにnext.config.jsを書き換えます。Next13.5でビルドする場合はまず、以下のように下準備が必要です。
module.exports = {
output: "export",
trailingSlash: true,
}
あとはコマンドを実行するだけです。outというフォルダが作成されれば、ビルドは成功しています。
# npm run build
もう一度、さっきのコマンドを実行すると、先ほどと別の結果が表示されています。