Nuxtで開発中、ページ遷移時に強制リロードされる事象に立ち向かう
TL;DR
✨ new dependencies optimized: <package-name>
✨ optimized dependencies changed. reloading
Nuxtでページ遷移時にこのようなログが出る場合、nuxt.config.tsに以下の設定を入れると解消します。
export default defineNuxtConfig({
vite: {
optimizeDeps: {
entries: [
// 実用的なスコープならこの3つくらい。もし必要なら.tsも。
"pages/**/*.vue",
"layouts/**/*.vue",
"components/**/*.vue",
// または、面倒なら全て
"**/*.vue",
]
}
}
})
はじめに
遭遇した事象
このサンプルでhttp://localhost:3000
を開くとリロードが発生し、ターミナルのコンソールにこのようなログが出ます。また、ページ遷移時にもリロードが発生する場合があります。
✨ new dependencies optimized: <package-name>
✨ optimized dependencies changed. reloading
開発中に意図せぬリロードが発生するのは開発者体験を損なうので、一体何が起きているのかを調べてみました。
Viteによる依存関係の事前バンドル
NuxtはデフォルトではViteを使ってアプリケーションをビルドします。そのため、この事象はViteが深く関わっています。
Viteはビルドする際に、依存関係(≒npmパッケージ)を事前バンドルします。事前バンドルについては長くなるのでドキュメントを読んでください。
Nuxtでは、実際に事前バンドルされているファイルはnode_modules/.cache/vite/
で確認できます。
この依存関係の事前バンドルですが、アプリケーションすべての依存関係を事前バンドルするわけではなく、必要最低限な依存関係を自動的に検出して都度事前バンドルするようです。
つまり、アプリケーションの開発中に事前バンドルが発生することがあり得ます。それが今回の事象だと考えられます。
検証してみる
では色んなパターンの事前バンドルの挙動を確認してみましょう。ずらっとパターンを列挙してみます。それぞれのパターンごとに別々のnpmパッケージを読み込む必要があるので、lodashを使ってlodashのモジュールが事前バンドルされるかどうかを検証していきます。
わかりやすくするために、各パターンA〜Hとlodashのモジュールの頭文字を対応させています。
パターン | 内容 | 依存関係 |
---|---|---|
A | app.vueでimport する依存関係 |
lodash.after |
B | app.vueでimport するコンポーネントが持つ依存関係 |
lodash.before |
C | app.vueで動的import する依存関係 |
lodash.compact |
D | app.vueで動的import するコンポーネントが持つ依存関係 |
lodash.debounce |
E | pages/index.vueが持つ依存関係 | lodash.every |
F |
NuxtLink (prefetchあり)の遷移先のpagesコンポーネントが持つ依存関係 |
lodash.flip |
G |
NuxtLink (prefetchなし)の遷移先のpagesコンポーネントが持つ依存関係 |
lodash.get |
H | layoutsコンポーネントが持つ依存関係 | lodash.head |
また。app.vueはこのようになっています。
これでサンプルリポジトリでnpm run dev
しましょう。
ここで注意するのは、ビルドするたびにnode_modules/.cache/vite
を削除することです。サンプルリポジトリではnpm run dev
すると勝手に消すようになっています。
npm run dev
の直後(http://localhost:3000
はまだ表示しない)
1. node_modules/.cache/vite
を見てみると、このようになっており、パターンA・B・Cが事前バンドルされている事がわかります。
_metadata.json
も覗いてみましょう。ここにはいろんなハッシュ値が保存されています。
{
"hash": "5d03508b",
"configHash": "7e260cbf",
"lockfileHash": "9443f8d2",
"browserHash": "19d7d7db",
"optimized": {
"lodash.after": {
"src": "../../../../lodash.after/index.js",
"file": "lodash__after.js",
"fileHash": "732b2acb",
"needsInterop": true
},
// ...
}
http://localhost:3000
を表示した直後
2. パターンD・E・Fから依存関係が新たに検出され、再バンドルが行われてページリロードが発生します。
_metadata.jsonも見てみると少し差分が出ています。
{
"hash": "5d03508b",
"configHash": "7e260cbf",
"lockfileHash": "9443f8d2",
- "browserHash": "19d7d7db",
+ "browserHash": "ab389ed8",
"optimized": {
"lodash.after": {
"src": "../../../../lodash.after/index.js",
"file": "lodash__after.js",
- "fileHash": "732b2acb",
+ "fileHash": "988c2045",
"needsInterop": true
},
// ...
}
http://localhost:3000/prefetch
に遷移した後
3. すでに先程パターンFの依存関係はバンドルされていたので、/prefetchに遷移しても再バンドルは発生しません。
http://localhost:3000/no-prefetch
に遷移した後
4. プリフェッチされてないパターンGで新たな依存関係が検出され、再バンドルが発生します。
{
"hash": "5d03508b",
"configHash": "7e260cbf",
"lockfileHash": "9443f8d2",
- "browserHash": "ab389ed8",
+ "browserHash": "011a43dd",
"optimized": {
"lodash.after": {
"src": "../../../../lodash.after/index.js",
"file": "lodash__after.js",
- "fileHash": "988c2045",
+ "fileHash": "4ad82157",
"needsInterop": true
},
// ...
}
http://localhost:3000/layout
に遷移した後
5. レイアウトコンポーネントを使うパターンHから新たな依存関係が検出され、再バンドルが発生します。
{
"hash": "5d03508b",
"configHash": "7e260cbf",
"lockfileHash": "9443f8d2",
- "browserHash": "011a43dd",
+ "browserHash": "3c84cb7d",
"optimized": {
"lodash.after": {
"src": "../../../../lodash.after/index.js",
"file": "lodash__after.js",
- "fileHash": "4ad82157",
+ "fileHash": "9baad5cf",
"needsInterop": true
},
// ...
}
ページリロードを防ぐには
一度バンドルされれば、node_modules/.cache/vite
のファイルが残っているので、再バンドルされない限りはリロードが起きません。
ただし、開発でブランチを切り替える際などで依存関係をリセットするためにnpm ci
のようなコマンドを実行すると、node_modulesが空になり、事前バンドルされたキャッシュファイルは消えます。
そうすると、Nuxtアプリケーションを立ち上げてページ遷移するたびに依存関係の再バンドルに遭遇してしまいます。
できれば最初の事前バンドルで、必要な分の依存関係を事前バンドルしたいですよね。
先ほど検証したパターンで、事前バンドルされなかったパターンD〜Hまでは、それぞれ以下のパスのコンポーネントでした。
パターン | ディレクトリ |
---|---|
D | components/ |
E・F・G | pages/ |
H | layouts/ |
これらのパスのコンポーネントの依存関係を事前バンドルしてもらうために、今回はoptimizeDeps.entries
を使って対策します。tinyglobby(glob)パターンを指定できます。
NuxtでViteの設定をするにはnuxt.config.tsでvite
プロパティを使います。
export default defineNuxtConfig({
vite: {
optimizeDeps: {
entries: [
"pages/**/*.vue",
"layouts/**/*.vue",
"components/**/*.vue",
]
}
}
})
これでもう一度サンプルリポジトリで、node_modules/.cache/vite
削除してnpm run dev
した直後にnode_modules/.cache/vite
を見てみると、事前バンドルされる事がわかります。
これで依存関係の再バンドル時のページリロードを防ぐことができました🥳
ちなみに①
NuxtのIssueコメントによると、この解決方法はRemixやSvelteKitでも採用されているようでした。
ちなみに②
依存関係が事前バンドルされてない状態でnpm run dev
すると、事前バンドルがある2回目以降のビルドに比べ、事前バンドルがない初回のビルドが遅くなりそうです。
そう思って試してみましたが、サンプルリポジトリでは初回のビルドと2回目のビルド時間がほぼ同じでした。
著者が実際に業務で開発している事前バンドルされる依存関係が300ファイル程度のアプリケーションで同じような設定にしても、初回ビルドと2回目以降のビルドで3200ms程度の差でした。初回だけですし、この程度なら受け入れられるのではないでしょうか。
少なくともページ遷移のたびにリロードが発生するよりは良いと思います。
ちなみに③
optimizeDeps.include
を使う方法も先程のIssueに上がっていましたが、すべての依存関係をここに並べるのでかなり冗長になります。
export default defineNuxtConfig({
vite: {
optimizeDeps: {
include: [
"lodash.after",
"lodash.before",
"lodash.compact",
"lodash.debounce",
"lodash.every",
"lodash.flip",
"lodash.get",
"lodash.head",
]
}
}
})
また、この方法はメンテナー曰くおすすめできないそうです。(もしかしたらoptimizeDeps.entries
も同じ問題を抱えているかもしれませんが。。。)
余談:Nuxtを使わず、VueとViteでビルドするときとの違い
冒頭にも少し書きましたが、ここまで書いてきた内容はNuxt固有の事象です。
Nuxtを使わずに、VueをViteでビルドすると全く異なる結果になります。
↑のサンプルではNuxtLink
のprefetch(パターンG)やNuxtのlayoutコンポーネント(パターンH)などは無いので省略しています。その他いくつか違う点があります。
おわりに
書き始めは、よくわからないけどとりあえずoptimizeDeps.entries
を使えば対策できるぞ!という内容で記事を書こうとしていましたが、ちょっと掘り下げてみたら色々なパターンがあり大きめなボリュームになりました。
Viteの事前バンドルの処理はかなり難しく、むやみに立ち向かっても理解するまでとても時間がかかりそうでした。有識者の解説記事を心待ちにしています。
Discussion