* fix(app-router): exclude dev overlay from production client bundle
* fix(app-router): reuse loaded dev overlay during HMR
---------
Co-authored-by: James <james@eli.cx>
* perf(client): omit unused server action client
* fix(client): capture server action state before lazy load
---------
Co-authored-by: James <james@eli.cx>
The vinext:image-imports transform rewrote `import X from './a.png'` into
two imports plus `const X = {...}`. The original import is a module-scoped
binding initialized before module-body execution, but the `const` is
block-scoped and TDZ-bound until its textual line, so any reference to `X`
that runs earlier — a hoisted function called above the import, or a
circular-import re-entry — flipped from working to a runtime
`Cannot access 'X' before initialization`.
Emit `var X = {...}` instead: it hoists like the import it replaces, so the
forward reference no longer throws (it reads `undefined` before the line,
strictly better than crashing). Matches Next.js, which leaves the consumer's
`import X` untouched and rewrites the image module itself.
Closes#1975
next/dynamic's ssr:false server branch rendered the loading component with
pastDelay:false, while the client's first (pre-mount) render and Suspense
fallback default pastDelay to true. For a loading component that branches on
pastDelay (the documented `if (!pastDelay) return null` pattern), the server
emitted nothing but the client rendered the spinner, causing a React hydration
mismatch and a visible content jump.
Drop the hardcoded pastDelay:false override on the server so it defaults to
true, matching both the client render and Next.js App Router, which always
renders the loading fallback with pastDelay=true on server and client. Loading
components that ignore pastDelay are unaffected.
Update the three tests that asserted the buggy server-side pastDelay:false, and
add a server-equals-client-first-render parity assertion.
Fixes#1967
The progressive (form-state) non-redirect server-action path returned the
raw `getAndClearPendingCookies()` array, so calling `cookies().set(name, ...)`
twice for the same name emitted two `Set-Cookie` headers for that name on
no-JS form submissions. Next.js stores mutable cookies in a name-keyed
`ResponseCookies` map (last value wins) and collapses to one header, as do the
redirect and RSC action paths in vinext via `dedupePendingCookies()`.
Wrap the non-redirect branch's cookie collection in `dedupePendingCookies()`
to match the sibling paths, so same-name sets collapse to a single Set-Cookie
(last value wins). The single binding feeds both the action-failed and success
returns, covering both non-redirect sub-cases.
Follow-up to the #1481 dedup fix, which landed on the redirect and RSC paths
but missed the progressive non-redirect branch.
Adds a regression test mirroring the existing redirect-dedup test for the
non-redirect form-state path.
createRscOnErrorHandler treated ANY error carrying a digest field as a
control-flow signal: it returned String(error.digest) and skipped
reportRequestError, so a genuine server-render error that merely carried a
digest (e.g. a hashed digest from sanitizeErrorForClient, or one transported
from a nested boundary) was silently swallowed with no instrumentation and
its digest shipped to the client as if it were a signal.
Mirror Next.js getDigestForWellKnownError: add a helper that recognizes only
genuine signals (redirect / notFound+http-fallback / bailout-to-CSR /
dynamic-server) and gate the short-circuit on it. Non-signal digest errors now
fall through to reportRequestError and keep their existing digest as-is.
Fix the same conflation in createAppPageRscErrorTracker.onRenderError, which
routed any digest-bearing error to the special 307/404-swap slot; gate it on
isNavigationSignalError so real errors reach the error boundary and a later
real signal can still win the slot.
Closes#1978
* perf(app-router): defer layout, template and boundary modules to first request
The generated RSC entry statically imported every route's layout, template,
loading/error/not-found/forbidden/unauthorized boundary and parallel-slot
module at the top level. On a cold isolate — and on the dev server's first
request — that eagerly evaluated the whole app's segment tree, including
routes never visited in that lifetime. Page and route-handler modules were
already lazy; the surrounding segment modules were not.
Emit those modules as lazy `() => import()` loaders paired with `null`
placeholders in the manifest, and hydrate them onto the route's synchronous
fields in `ensureAppRouteModulesLoaded` before any read (extended from
page/handler to layouts, templates, boundaries and parallel-slot modules).
Intercept layouts hydrate via the dedicated `loadAppInterceptLayouts` because
they live on the match object, not the route. Every reader already runs behind
`ensureAppRouteModulesLoaded` (handler dispatch and `buildPageElements`) or the
intercept resolvers, so no synchronous module field is read before hydration.
The shared intercept `interceptLayouts` arrays keep their `readonly` public
types; the loader is their sole writer and fills slots through one
boundary-local cast, so route metadata stays immutable to callers.
Dev cold start is unchanged on page-heavy apps — pages were already lazy — but
on a boundary-heavy app (80 independent sections, each with
layout+template+error+loading+not-found) first-request module compile drops
704ms -> 534ms (~24%) and time-to-first-200 1917ms -> 1665ms. The saving scales
with the number of unrelated segments deferred.
* test(app-router): localize react max header font fixture
* fix(app-router): dedupe lazy intercept module loads
* fix(app-router): share intercept load state across matches
* docs(app-router): explain root layout loader reuse
---------
Co-authored-by: James <james@eli.cx>
* fix(perf): preserve permissioned cache roots
* fix(perf): run trusted same-repo harness
* fix(perf): ignore reaped benchmark zombies
* fix(perf): profile the build process tree
* fix(perf): wait for benchmark process cleanup
* fix(perf): profile the real build command
* fix(perf): profile the Vite Plus build entry
* fix(perf): avoid nested profiling sudo
* fix(perf): validate traces across profiles
* fix(proxy): match Next.js file conventions
* test(proxy): keep convention fixture out of workspace
* test(proxy): provide middleware path in handler fixture
sendCompressed wrote the full body for every request. HEAD requests routed
through it (bot/crawler-buffered Pages HTML and Pages API routes) still
compressed and emitted a payload that Node then discards at the socket level.
Short-circuit with res.end() after writeHead when req.method is HEAD,
mirroring sendWebResponse. This skips spinning up a compressor for a body
that would be thrown away and keeps the buffered sender consistent with the
streamed and static-file senders, which already handle HEAD.
Co-authored-by: James <james@eli.cx>