Next.jsでrewritesを使うと全く関係ないページで再レンダリングが発生する
【まとめ】Next.jsのrewritesは気軽に使わない方が良いかも
- 一つでもrewrites設定が書かれているとクライアントサイド含め色々な処理が発生する
- rewrites設定が一つでも書かれていると、rewritesのパターンにマッチしないページであっても再レンダリングが発生することがある
- JSのバンドルサイズが大きくなる可能性あり(next@v11.0.2 でrewriteをひとつ記述し
next build
したところmain.js
が約4KB増えた)
Next.js v11.1.2で確認
next.config.jsでrewrites
を指定すると、一部のページで初回読み込み時に再レンダリングが発生することが分かった。
rewrites() {
return {
afterFiles: [
// ここで設定する値に関わらず問題が発生する
{
source: '/anypath',
destination: '/anypath/foo',
},
],
};
},
例えば_app.tsx
コンポーネントの中でconsole.log("rendered")
とか書いて、ブラウザをリロードするとrendered
が2回出力される。rewrites
設定にマッチしない場合であってもページも再レンダリングされてしまうケースがある。
rewrites
設定を空にするとレンダリングは1度だけになる。
Issueなどなどを探してもこの問題についての記述がまったく見つからなかったので調査内容をメモっていく。
Next.jsの過去のバージョンでこの問題が発生するか調べてみた。すると
- v10.1.x では発生しない
- v10.2.x で発生する
ということが分かった。
とりあえずこの差分を見てみるのが良さそう。
↓ v10.1.3とv10.2.0の差分
該当箇所を見つけた
// We need to replace the router state if:
// - the page was (auto) exported and has a query string or search (hash)
// - it was auto exported and is a dynamic route (to provide params)
// - if it is a client-side skeleton (fallback render)
if (
router.isSsr &&
// We don't update for 404 requests as this can modify
// the asPath unexpectedly e.g. adding basePath when
// it wasn't originally present
page !== '/404' &&
!(
page === '/_error' &&
hydrateProps &&
hydrateProps.pageProps &&
hydrateProps.pageProps.statusCode === 404
) &&
(isFallback ||
(data.nextExport &&
(isDynamicRoute(router.pathname) ||
location.search ||
process.env.__NEXT_HAS_REWRITES)) ||
(hydrateProps &&
hydrateProps.__N_SSG &&
(location.search || process.env.__NEXT_HAS_REWRITES)))
) {
// update query on mount for exported pages
router.replace(
router.pathname +
'?' +
String(
querystring.assign(
querystring.urlQueryToSearchParams(router.query),
new URLSearchParams(location.search)
)
),
asPath,
...
rewritesが指定されていると、このrouter.replace()
が不要な場合にまで発火してしまっているのだと思われる。
問題が発生する原因となったPRはこちら。この変更により「rewrite時にクエリパラメータが適切に取得できない」という別の問題が解消されている。
再レンダリングが発生するときには次の条件がtrue
になってしまっていることが分かった。
(data.nextExport &&
(isDynamicRoute(router.pathname) ||
location.search ||
process.env.__NEXT_HAS_REWRITES))
-
data.nextExport
はgetInitialProps
やgetStaticProps
、getServerSideProps
などを持たないページでtrueになる。 -
process.env.__NEXT_HAS_REWRITES
は何らかのrewrites設定を書くとtrueになる
つまりrewrites
を書くとget**Props
を持たないページすべてが再レンダリングされることになる。
思いのほか解決が難しそうな問題だった
Next.jsのコードを眺めていて分かってきたこと
- Next.jsでは一つでもrewrites設定が書かれていると、グローバルに参照される
process.env.__NEXT_HAS_REWRITES
がtrue
となる(ちなみにクライアントサイドだと別名の変数になっている) - この値が
true
だと後続の処理でリライトに関わる処理が呼ばれる- 上述の
router.replace()
もその一つ(URLクエリパラメータを適切に取得するために必要) - もしかすると他にもこのようなワークアラウンド的な処理が書かれている or 今後書かれるかもしれない
- 一つでもrewrites設定が書かれていると、余計な処理が入る or 余計なJSファイルが読み込まれる可能性がある
- 上述の
- CDNにキャッシュされる静的なページでは、ページデータがCDNから直接配信されるため、オリジンサーバーでリライトの処理を噛ませることができない → クライアント側でリライトの処理を行うしかない
- 動的ルーティングのページもリライトの対象とすることが可能であるため、リライトのパスごとにあらかじめキャッシュを作るようなことは難しい
Next.jsリポジトリにIssueをあげようと思ったが、構造的に解決が難しそうであることが分かったため見送ることにした。おそらくNext.jsの開発チームの人たちもこの問題は認識しているのではないかな。
rewriteを一つでも書くとbundle sizeが大きくなる
next build
+ next start
で動かしてみると、やっぱりbundle sizeに影響があった。
↓ rewrites設定を一つだけ書いた場合(どのページにもマッチしないrewrites設定にした)
↓ rewrites設定を消した場合(それ以外は全く同じ)
v11.0.2では約4KB程の違いが出た。
Vercelにデプロイした場合にはもしかすると違う結果になるかも