This is a Next.js project bootstrapped with create-next-app
.
This repository demonstrates an issue with the next/link
component regarding the default behavior of prefetching the Loading UI of links pointing to dynamically rendered routes.
Note: Use Firefox Developer Edition to inspect the prefetched responses for each <Link>
visible on the homepage.
The documentation for next/link
states the default behavior as "Prefetch behavior depends on whether the route is static or dynamic. For static routes, the full route will be prefetched (including all its data). For dynamic routes, the partial route down to the nearest segment with a loading.js boundary will be prefetched." - See https://nextjs.org/docs/app/api-reference/components/link#prefetch
This bug negatively affects the user experience by diminishing the perceived performance of navigating to dynamically rendered pages when using the default prefetch behavior. When users click a <Link>
, the browser fails to display the loading.js
fallback immediately and instead waits for it to be streamed after the navigation request is made. This delay in showing the loading UI can lead to a noticeable performance degradation, especially for users on slower networks or devices.
Although the issue doesn't occur with all routing configurations, it poses a significant challenge for medium to large-scale websites that rely on dynamic rendering and have an app routing configuration beyond the most basic level, ie using route groups, or nested routes with dynamic segments underneath. Additionally, sites that rely on top-level dynamic route segments to support internationalization, ie [locale]
, are particularly affected as this issue impacts all dynamically rendered pages that rely on the default prefetch behavior.
The below cases show where the prefetching feature of <Link>
fails to correctly fetch the loading.js boundary of each page. The response from the prefetch request to the server does not contain the loading.js fallback.
- Using a Route Group, ie
(example-route-group)/broken
- Using a route segment, ie
example/broken2
- Using a dynamic segment underneath a route segment, ex
/products/[slug]
Example of failed prefetch response that is missing the loading.js
fallback:
// from page: (example-route-group)/broken
0:["O4DIzrPgeHEefeF_dZWuy",[["children","(example-route-group)",["(example-route-group)",{"children":["broken-route-group",{"children":["__PAGE__",{}]}]}],null,null]]]
The below cases show where the prefetching feature of <Link>
successfully fetches the loading.js boundary of each page.
- Using a top level dynamig segment, ie
/[slug]
- Using a top level route segment, ie
/works
Example of a successful prefetch response that contains the loading.js
fallback:
// from page: /3
1:I[5851,[],""]
3:I[841,[],""]
2:["slug","3","d"]
0:["O4DIzrPgeHEefeF_dZWuy",[["children",["slug","3","d"],[["slug","3","d"],{"children":["__PAGE__",{}]}],[["slug","3","d"],{"children":null},[null,["$","$L1",null,{"parallelRouterKey":"children","segmentPath":["children","$2","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],[["$","div",null,{"children":[["$","h2",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}],["$","h3",null,{"children":"Loading..."}]]}],[],[]]],["$L4",null]]]]
4:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Create Next App"}],["$","meta","3",{"name":"description","content":"Generated by create next app"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
The application must be run in production mode in order for the next/link
prefetching to occur.
- build the application with
pnpm build
- run with
pnpm start
See the below responses of each prefetch, taken with Firefox Developer Editon: