next.js
2521b8ad - [Instant] Instant validation in Dev (#89077)

Commit
25 days ago
[Instant] Instant validation in Dev (#89077) This PR implements the initial version of dev-time validation for `export const instant = ...` ```tsx export const instant = { prefetch: 'static' } ``` ```tsx export const instant = { prefetch: 'runtime', ... } ``` ```tsx export const instant = false ``` (the config is currently expected to be named `unstable_instant`, but i'll be shortening it to `instant` below.) When a segment specifies an instant config with `prefetch: 'static'` or `prefetch: 'runtime'`, we'll validate that all (prefetched) navigations into that segment will render instant UI, i.e. that the navigation won't block. On the other hand, if a segment is allowed to block, it must be marked with `export const instant = false`. Note that our existing static shell validation is a special case of this, but for now, we're keeping both. A future PR will reimplement static shell validation as a special case of instant validation. Like static shell validation, these validations currently run in dev whenever we render the full page (i.e. no segments are omitted). This means it'll happen on an initial load (or refresh) and HMR. ### Implementation notes #### Validation approach The goal is to simulate what the browser would display for a prefetched client navigation. We do this by re-assembling the segments extracted from the original stream for the page. - The outer (shared) segments should already fully resolved (i.e. in the Dynamic stage), to represent the fact that the browser loaded them before - The new segments are either in Static or Runtime stage, depending on how each segment would have been prefetched. Similar to static shell validation, we then perform a Fizz prerender on the combined payload, abort it, and track the locations of `onError` calls. The difference here is that unlike static shell validation, we require a Suspense boundary *inside* the new subtree. This is detected by adding a `InstantValidationBoundary` around the new subtree when we construct the payload, and checking if there's a suspense below that in the component stack. Currently, for each `instant` config we find, we take each of its parent layouts as a possible navigation parent (unless it's marked as blocking). So if we have a page with parent layouts like this: ``` /layout.tsx /foo/layout.tsx /foo/bar/layout.tsx /foo/bar/page.tsx <- `export const instant = { prefetch: 'static' }` ``` Then we'll check for navigations where each of `/layout.tsx`, `/foo/layout.tsx` and `/foo/bar/layout.tsx` is a fully resolved shared parent. If a validation fails, we do a "discriminated error message" flow analogous to static shell validation. Static segments are replaced with Runtime segments to see if the error goes away, which lets us determine if the hole is caused by runtime or dynamic data. #### Building the combined payload First, we need to separate the full stream into segments. The process is similar to `collectSegmentData` -- we need to deserialize the payload from the stream and then re-serialize each segment separately. The complexity here comes from the fact that the full stream is separated into stages, and we need each segment to be in staged form as well. This lets us pick and choose which stage the segment is in to simulate what we'd show for a static or runtime prefetch. The other complex part is the "late release trick" (as implemented in `createNodeStreamWithLateRelease`). This was already done for static shell validation, but it's more complex here. Debug info for dynamic holes is usually delayed until a further stage. So, in the static stage we only see an unresolved promise (or lazy reference), and then in the runtime stage we get the debugInfo telling us that it was caused by `await cookies()`. To ensure that this debug info is available for purposes on error reporting, we do a "late release" -- before we abort, we advance the each segment to the dynamic stage, which won't cause any new content to render, but will provide Fizz with debug info and thus give us precise error locations in `onError`. If you see `releaseSignal`, that's what it's for. #### `instant` on layouts We support specifying `instant` on layouts. The validation principle is the same, but note that in the current mode (validating a single page in dev) we can't enforce that *all* navigations into that layout satisfy the constraint, we can only check if that's satisfied for the current page. #### Blocking segments A segment can be marked as blocking with ```tsx export const instant = false ``` This is meant to signal that this segment deliberately does not have instant UI. Currently, this is affects validation as follows: If there's no `instant` config in parent segments, skip validating all navigations where this layout would be new (because we know it'd block), i.e. only validate navigations where it's a shared parent. Children of this segment can still have `instant` configs of their own, and those will be validated. This allows structures where a layout is blocking, but once it's loaded, navigations within it should have instant UI. If there is an `instant` config in a parent segment, the validation of that parent is *not* skipped. The reasoning here is that if a layout asserts that navigations into it should have static UI, then a child with `instant = false` should not violate that. This essentially requires that the child have a `loading.js` or that the layout has to wrap the slot with Suspense. Note that `instant = false` can also be used to opt segements out of static shell validation without requiring a Suspense above body. ### Limitations and planned follow-ups - `instant` configs in parallel routes (i.e. `@slot`) are not currently validated. - `expectUnableToVerify` is not implemented yet, i.e. there's no way to say "this cannot be validated using SSR". The shared segments are not expected to suspend. For now, `export const instant = { prefetch: ..., unstable_disableValidation: true }` can be used to bypass a segment if validating it causes problems. - static shell validation is a special case of instant validation, but is not currently implemented as such. - we should be able to validate during client navigations, not just full-page loads and HMR, but we currently can't because we don't have all the segments when we do that, so we can't assemble a proper combined payload.
Author
Parents
Loading