Skip to content

test(next): preview Next.js 16.3 instant navigations [DO NOT MERGE] - #259

Draft
arzafran wants to merge 31 commits into
mainfrom
next/16.3-preview
Draft

test(next): preview Next.js 16.3 instant navigations [DO NOT MERGE]#259
arzafran wants to merge 31 commits into
mainfrom
next/16.3-preview

Conversation

@arzafran

@arzafran arzafran commented Jun 26, 2026

Copy link
Copy Markdown
Member

⚠️ Preview only — do not merge

Rides the @preview npm tag, which is pre-production. Stays a draft until 16.3 ships stable.

What this does

Tracks Next.js 16.3.0-preview.10 and turns on the instant-navigation config, so the deploy preview exercises shell-based prefetching against a real deployment.

  • next + @next/bundle-analyzer pinned to 16.3.0-preview.10
  • partialPrefetching: true alongside the cacheComponents: true the starter already had. Prefetching moves from per-link to per-route: one reusable shell per route, cached client-side and shared by every link pointing at it, instead of a request per link in the viewport
  • app/loading.tsx is a real accessible skeleton shell rather than a placeholder word, so navigations land on something meaningful
  • Carries current main, which now includes the Biome to oxlint + oxfmt migration (build: migrate from Biome to oxlint + oxfmt #309). This branch lints and formats exactly like main; the demo routes under app/instant-nav-demo/ were written under Biome and are reformatted by oxfmt here

Dev server got measurably faster in preview.9

preview.9 picks up React's stack-trace-limit optimisation, which Vercel measured at up to 73% off RSC render time in dev. Measured here on satus's home route, warm, 25 requests each, A/B/A to rule out ordering effects:

Build Median request Next-reported render
preview.8 102.7ms ~82ms
preview.9 63.3ms ~55ms
preview.8 (re-run) 101.0ms ~85ms

About 38% off the home route. Less than the headline 73% because satus's landing page is small and a chunk of the remaining time is fixed request overhead, not RSC render.

Measured on preview.9 when it landed. Not re-run for preview.10, which carries no dev-render change worth re-benchmarking.

One thing worth knowing: the win is V8-only. It does not apply under JSC, so a Bun-hosted dev server sees none of it. satus is safe today because bun next dev honours the #!/usr/bin/env node shebang and hands off to Node, verified in the process tree:

bun ./lib/scripts/dev.ts
└── bun next dev
    └── node node_modules/.bin/next dev   ← V8, so the optimisation lands

If anyone ever "tidies" lib/scripts/dev.ts to force Bun's runtime, dev RSC rendering silently gets slower. Worth a comment on main guarding that.

New in preview.9: next dev writes to AGENTS.md

next dev now appends a managed <!-- BEGIN:nextjs-agent-rules --> block to AGENTS.md (or scaffolds AGENTS.md + CLAUDE.md if neither exists). It fires only when @vercel/detect-agent sees an AI coding agent in the environment, so a human running bun run dev never sees it, but any agent-driven dev run does. There is no config opt-out; the gate is in app-info-log.js, the writer in server/lib/generate-agent-files.js.

Reverted on this branch rather than committed. satus's AGENTS.md is the curated house-standards doc that every fork inherits, so accepting Next's boilerplate into it is a call the team should make deliberately, not one a dev-server side effect makes for us. Added to the merge checklist below.

What to look at on the preview

  • Click between routes — navigations should feel SPA-instant, shell first with data streaming in
  • Network tab: one reusable shell per route, not one request per link

Known issues in the 16.3 line

Verified against the changelog and the tracking discussion, current as of 2026-07-23:

  • Instant Insights devtools are broken in Safari. Use Chrome or Firefox when evaluating
  • cacheComponents: true breaks AWS/SST v4.17 deploys entirely. Only matters for a client not deploying to Vercel, but it is a hard failure, not a degradation — worth knowing before this becomes a starter default
  • turbopackIgnore fails on nested expressions, which can oversize standalone bundles
  • Default link prefetching may include runtime data without prefetch='allow-runtime', contradicting the docs. Unresolved upstream

No breaking changes in the 16.3 line affect this stack. The 16.0 renames (middlewareproxy, dynamicIOcacheComponents, image defaults) are already absorbed.

Worth trialling separately

  • experimental.turbopackFileSystemCacheForBuild — CI build-time win, no code-pattern change
  • experimental.turbopackRustReactCompiler — drop-in swap for the Babel React Compiler plugin, worth benchmarking

Before this could merge

  • Next.js 16.3 reaches stable — Vercel said 'a few weeks' on 2026-07-17, no RC phase, no date
  • Repin from @preview to the exact stable version
  • Delete app/instant-nav-demo/ — demo routes are branch-only and must never reach main
  • Decide whether to accept or strip Next's managed AGENTS.md block
  • Decide whether partialPrefetching becomes a starter default, given the AWS/SST caveat

Verified

  • bun run check passes → 385 tests, on the oxc toolchain
  • bun run build succeeds on preview.10, from a cleared .next

@arzafran arzafran added the preview Testing/preview branch — generates a deploy preview, not for merge label Jun 26, 2026
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
satus Ready Ready Preview Jul 29, 2026 11:01pm

Throwaway, branch-only routes under app/instant-nav-demo/ (hub + fast/slow/[item])
to exercise 16.3 shell prefetching + partial streaming on the preview deploy.
Also enables experimental turbopackFileSystemCacheForBuild and
turbopackRustReactCompiler. Verified: build + typecheck + lint green on 16.3.0-preview.5.
# Conflicts:
#	CHANGELOG.md
#	bun.lock
#	package.json
Brings the oklch colour system, the style drift guard, the contrast gate
and its baseline, the palette prune, and the dependency roll-up onto the
preview branch. package.json conflicts resolved by taking main and
re-pinning only next and @next/bundle-analyzer to the preview tag;
bun.lock regenerated rather than hand-merged.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Previews Next.js 16.3 “Instant Navigations” behavior in a deploy preview by pinning next to a preview dist-tag, enabling shell-based prefetching, and adding demo routes + a real loading shell to evaluate navigation feel and streaming behavior.

Changes:

  • Bump next / @next/bundle-analyzer to 16.3.0-preview.6 for preview testing.
  • Enable partialPrefetching: true and update Next 16.3-related experimental flags in next.config.ts.
  • Replace the root app/loading.tsx placeholder with an accessible skeleton and add a throwaway app/instant-nav-demo/* route set to exercise instant navigations.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
package.json Pins Next.js + analyzer to 16.3 preview versions for deploy preview testing.
next.config.ts Enables partialPrefetching and updates the 16.3 preview experimental flags.
CHANGELOG.md Documents the Next 16.3 preview changes and the throwaway demo routes.
app/loading.tsx Upgrades root loading UI to an accessible skeleton shell.
app/instant-nav-demo/README.md Documents the demo routes and the “do not merge” intent.
app/instant-nav-demo/page.tsx Demo hub route to exercise shell-prefetch navigation.
app/instant-nav-demo/fast/page.tsx Static demo route for “shell == page” navigation.
app/instant-nav-demo/slow/page.tsx Streaming demo route showing partial rendering under <Suspense>.
app/instant-nav-demo/[item]/page.tsx Dynamic demo route that suspends and shows a shared loading shell.
app/instant-nav-demo/[item]/loading.tsx Loading shell for the dynamic demo route.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/instant-nav-demo/slow/page.tsx Outdated
Comment thread app/instant-nav-demo/fast/page.tsx Outdated
Comment thread app/instant-nav-demo/page.tsx Outdated
Comment thread app/instant-nav-demo/[item]/page.tsx
Comment thread app/instant-nav-demo/[item]/page.tsx Outdated
Comment thread app/instant-nav-demo/[item]/loading.tsx Outdated
arzafran added 2 commits July 22, 2026 16:48
React's own types now document FormEvent as deprecated and point at
SubmitEvent, and DOMAttributes already types onSubmit as
SubmitEventHandler, so this matches what React expects today rather than
only future-proofing. React 19.2.10 turns it into a deprecation warning.
Picks up the client-navigation <title> regression fix that landed in
canary.76.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

app/instant-nav-demo/[item]/page.tsx:14

  • params is typed as a Promise and then awaited, but Next.js page props pass params as a plain object. Keeping it as a Promise makes the type misleading and adds an unnecessary await (even though it happens to work at runtime).
}: {
  params: Promise<{ item: string }>
}) {
  const { item } = await params

app/instant-nav-demo/page.tsx:13

  • House style prefers h-dvh/min-h-dvh over h-screen/min-h-screen to avoid mobile viewport issues (AGENTS.md:95).
      <section className="dr-layout-grid min-h-screen content-center py-24 font-mono">

app/instant-nav-demo/fast/page.tsx:11

  • House style prefers h-dvh/min-h-dvh over h-screen/min-h-screen to avoid mobile viewport issues (AGENTS.md:95).
      <section className="dr-layout-grid min-h-screen content-center py-24 font-mono">

app/instant-nav-demo/slow/page.tsx:38

  • House style prefers h-dvh/min-h-dvh over h-screen/min-h-screen to avoid mobile viewport issues (AGENTS.md:95).
      <section className="dr-layout-grid min-h-screen content-center py-24 font-mono">

app/instant-nav-demo/[item]/page.tsx:20

  • House style prefers h-dvh/min-h-dvh over h-screen/min-h-screen to avoid mobile viewport issues (AGENTS.md:95).
      <section className="dr-layout-grid min-h-screen content-center py-24 font-mono">

app/instant-nav-demo/[item]/loading.tsx:6

  • House style prefers h-dvh/min-h-dvh over h-screen/min-h-screen to avoid mobile viewport issues (AGENTS.md:95).
      className="flex min-h-screen flex-col items-center justify-center gap-3 font-mono"

Comment thread app/instant-nav-demo/README.md Outdated
AGENTS.md calls for dvh over screen units; 100vh leaves the section
short by the browser chrome on mobile. Five demo routes were still on
min-h-screen.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

app/instant-nav-demo/README.md:13

  • The example URLs for the dynamic [item] route are inconsistent: /bravo and /charlie are missing the /instant-nav-demo/ prefix, which could confuse readers/testing.
- `[item]/` — dynamic route that suspends at the top level, so its `loading.tsx`
  shell is shown on the way in and reused identically for every param value
  (`/instant-nav-demo/alpha`, `/bravo`, `/charlie`)

…gions

Two real accessibility bugs and three semantic upgrades. Accessibility findings
are now at zero.

InputField and TextareaField only rendered <Field.Label> when a `label` prop
was passed, which is optional. A field given just a placeholder reached screen
readers with no accessible name at all. There is now a fallback chain: the
visible label if there is one, otherwise the placeholder, otherwise the field
name. The aria-label is deliberately NOT set when a visible label exists —
aria-label overrides it, and the announced name could then drift from the text
on screen.

The three role="status" loading regions become <output>, which carries that
role implicitly and gives screen readers more reliable semantics. Two of them
already had `flex` in their class list, which overrides <output>'s inline
default, so the layout is untouched. The third (the demo panel skeleton) had
only `w-40`, and width does nothing on an inline element, so it gains an
explicit `block`.

Two suppressions remain on the form controls, on the inner elements passed to
Base UI's `render` prop. Base UI merges id, name and the aria-label onto those
at runtime, so a static read of that line sees an unlabelled input that never
reaches the DOM. The name is always present.

react-doctor: 45 issues -> 40. Accessibility 5 -> 0. check passes, clean build.
Copilot AI review requested due to automatic review settings July 29, 2026 21:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

react-doctor is right that these two awaits are independent, so they run
concurrently now. The 1200ms delay below stays sequential — a slow route is
what the demo exists to show.
Copilot AI review requested due to automatic review settings July 29, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…t by folder

Uses react-doctor's ignore.overrides so these stop polluting every local run
and every CI log, without going blind in those files: the suppression lists
specific rules, so a genuine bug of some other kind is still reported. No
blanket "skip this folder" anywhere.

lib/dev/theatre is an integration with an external, imperative animation
editor. React does not own that data flow — values change because someone
dragged a keyframe in the studio UI, and the hooks exist to subscribe and
mirror it back. All eight rules presuppose React owning the flow, so each asks
for a rewrite that cannot exist. no-event-handler is the clearest: it says to
move the side effect into the event handler that triggers it, and there is no
React event handler, only Theatre's own notifications. It is also dev-only,
lazily imported, behind a dev toggle, and deleted by setup:project when Theatre
is dropped.

components/layout/theme already implements React's documented pattern for
adjusting state when a prop changes, render-phase re-sync with a prevTheme
sentinel, docs link in the file. The three rules there each ask for something
impossible: computing currentTheme inline (it is runtime-overridable via
setTheme), holding it in a ref (it is rendered, through context), or moving the
data-theme write off an effect (it synchronises an external system, which is
what effects are for).

Verified the scoping does not leak: exhaustive-deps still fires in
lib/integrations/hubspot/embed, so it is genuinely path-limited.

react-doctor: 39 issues -> 12, still zero errors.
Copilot AI review requested due to automatic review settings July 29, 2026 21:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The callbacks ran in an effect that watched formState and listed onSuccess and
onError as dependencies. Callers pass those inline, so a parent re-render handed
the effect fresh function identities, it re-ran, and it fired the callbacks a
second time for a submission that had already been handled. Anything
non-idempotent behind onSuccess — an analytics event, a redirect, a toast — ran
twice.

They now fire inside a wrapper around the action, where the result is already
known. Exactly once per submission, and there is nothing left to observe after
the fact.

The reset effect that survives also had a cleanup hole. `resetTimer` is
assigned inside the rAF write-queue callback, so unmounting before that queue
drained left the timer to be created after teardown, with setKey firing on a
component that no longer existed. A `cancelled` flag now covers that window.

Found by react-doctor's no-prop-callback-in-effect. Worth noting the rule
pointed at a lint concern and the actual payoff was two latent bugs.

check passes, clean build. react-doctor: 12 issues -> 10.
Copilot AI review requested due to automatic review settings July 29, 2026 22:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Lighthouse audits one cold page load, which cannot see the instant-navigation
work at all: partialPrefetching and cachedNavigations change how fast an
already-loaded app swaps routes, and a load audit never navigates. Confirmed
that the hard way — a 3x3 Lighthouse pass over this branch moved nothing
outside noise, because none of it was measuring the thing that changed.

`bun run bench:nav` clicks a real <Link> and records two numbers per run: time
to the target route's shell, and time to its streamed content. Content is the
control — it is server-bound by design, so if it moves, something other than
prefetching did it.

It throttles to 100ms RTT by default, and that default is load-bearing. On an
unthrottled localhost the RTT is ~0, and hiding round trips is the entire
reason prefetching exists, so the flags measure as useless-to-harmful. The
first version had no throttling and said exactly that.

What it found, 7 runs each with all four instant-nav flags toggled together:

                   flags off             flags on
  shell     110ms  (spread 84-250)   90ms  (spread 78-105)
  content  1420ms                   1915ms

The shell median barely moves; the spread is the story. An on-demand shell
fetch ranges 84-250ms, a prefetched one 78-105ms — prefetching buys
predictability more than raw speed, which is what "instant" actually means to
someone clicking a link.

The content cost is real, not noise: +537ms unthrottled and +495ms at 100ms
RTT. Splitting shell from data serialises them, so this route's 1.5s server
sleep starts only after the shell paints. That is a trade — an instant shell
plus skeleton against a shorter blank wait — and it is written into the
script's header so it does not get rediscovered.

Reports the full sample alongside the median, because a 3-run median on a dev
machine can be moved by a single outlier. That already bit once in this
session: a 3x3 Lighthouse pass appeared to show +285ms TTI, and at n=5 it was
one cold run in a small sample.
Copilot AI review requested due to automatic review settings July 29, 2026 22:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Third measurement angle, covering what the other two structurally cannot:
lighthouse audits one cold load and never re-renders; bench:nav measures route
swaps, not the work a mounted tree does when state changes. This drives
viewport changes and reads main-thread cost from CDP's Performance domain, so
it works against a production build rather than React's dev-only profiling
hooks.

It also corrects something I claimed earlier in this branch. Reverting only
lib/hooks/use-device-detection.ts to its pre-fix shape, 5 runs each, 30
viewport changes, 4x CPU throttle:

                  pre-fix                 fixed
  script   115ms  (spread 106-122)  121ms  (spread 99-157)
  task     850ms                    931ms

No improvement. I described that fix as the highest-value one of the set on the
grounds that a compiler bailout costs auto-memoization for the whole function.
That reasoning holds, but the magnitude does not: the hook's body is a few
media-query reads and boolean derivations, and losing memoization on work that
cheap costs nothing measurable. The resize cascade is dominated by style
recalculation (~250ms of ~900ms) and the WebGL canvas.

The fix stays — it removes a real cascading-render pattern and a genuine
bailout, and useSyncExternalStore is the correct shape for a
session-constant synchronous read. But it is hygiene, not a speedup, and the
script header says so, so the next person does not reach for a bailout fix
expecting a number to move.

Scope is documented too: the fluid and flowmap bailout fixes are not reachable
from any route (only FlowmapProvider consumes them, and nothing mounts it), so
no browser harness can measure them and this one does not pretend to.
Copilot AI review requested due to automatic review settings July 29, 2026 22:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

bench:nav was hardcoded to the instant-nav demo routes, which live only on this
branch, so it could not run against main at all — it just timed out waiting for
a heading that does not exist. It now takes --scenario, with the demo flow as
the default and a fallback that works on any build.

The fallback is the 404's "Go Home" link, because that is the only real
client-side navigation main has. Main's happy path has no navigable surface:
the only internal <Link>s are in error.tsx and not-found.tsx, there is no
header nav, and nothing links / to /sanity. Prefetching is a <Link> behaviour,
so with no links there is nothing to measure.

It deliberately reports no `content` number. / is statically prerendered and
has nothing streaming, and a second noisier reading of the same shell timing
dressed up as a different metric would be worse than no metric.

Comparing main (16.2.12, no flags) against this branch (16.3.0-preview.10, all
four flags):

  main    shell 116ms  (spread 83-223)
  branch  shell 107ms  (spread 81-327)

No signal — 9ms apart with ~40ms of run-to-run variation and one large outlier
each side. That is the expected result and it is the useful half: a static
route has no shell-versus-data split for partialPrefetching to make, so it is
equally prefetchable before and after. The flags target dynamic routes, which
is why the demo scenario shows a difference and this one does not. The header
says that explicitly, so nobody reads this scenario as proof the flags are
inert.
Copilot AI review requested due to automatic review settings July 29, 2026 22:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

main now carries the nine version-independent commits from this branch under
different SHAs (#316), so this merge is mostly git recognising the same content
arriving from both sides. Two conflicts, both from the deliberate divergences
made while cherry-picking:

- package.json: main took bench:rerender only, since bench:nav's useful
  scenario needs the instant-nav demo routes, which are branch-only. Resolved
  to keep both here, because here both exist.
- bench-rerender.ts: its header lists the sibling tools, and on main that line
  had to say bench:nav lives elsewhere. Restored the branch wording.

app/loading.tsx did not conflict, which was the point of keeping main's
`Cooking...` version during the cherry-pick rather than dragging over an
<output> conversion for markup main does not have.

Branch-only work is intact: the <output> loading regions, both instant-nav
flags, and the demo routes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Testing/preview branch — generates a deploy preview, not for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants