fix(app-router): stream ancestor loading boundaries#2331
Conversation
|
/bigbonk review for issues |
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
There was a problem hiding this comment.
Review: stream ancestor loading boundaries
I reviewed the implementation, ran the targeted unit suite (199 tests across app-route-graph, app-page-route-wiring, app-page-dispatch, app-route-module-loader, entry-templates — all green), and ran vp check on the 5 changed source files (clean). The change is well-scoped and the indexing/wiring is correct.
What's correct
- Tree-position parity.
discoverAncestorLoadingswalks the rawsegmentsarray withpath.joinand assignsindex + 1positions, matchingdiscoverSegmentErrorsexactly and aligning withcomputeLayoutTreePositions(both count all directory levels including route groups). Ancestor positions therefore interleave correctly with layouts/errors inorderedTreePositions. - No double-counting of the leaf. Root loading is only recorded when
segments.length > 0, and the loop stops atsegments.length - 1, so the route's ownloadingPath(handled separately at the route level) is never duplicated. Home page (/) and single-segment routes were verified by hand. - Manifest wiring mirrors the existing
errorPathspattern (placeholdermoduleArray+__loadAncestorLoadingslazy loaders + parallelancestorLoadingTreePositions), andensureAppRouteModulesLoadedhydrates positionally. Index alignment holds because both arrays derive from the same orderedancestorLoadingEntries. - Dispatch refactor is behavior-preserving.
hasActiveLoadingBoundaryalready gatedhasLoadingBoundaryand the priorprobePageSpecialErrorearly-return; the new formroute.loading?.default || route.ancestorLoadings?.some(...)is a strict superset of the old condition, and replacing the inlineprobePageSpecialErrorcheck withhasActiveLoadingBoundaryis equivalent (the suppress check is folded into the variable). The dispatch test now proves ancestor loadings skip the eager page probe and prevent dynamic-RSC reuse when the page readssearchParams. - Suspense placement sits between the segment's layout boundaries (outer) and error boundary (inner) per the documented per-segment nesting, keyed by
segmentResetKeyconsistent with the route-level and slot-level loading boundaries.
Minor observations (non-blocking)
- Prefetch loading shell now gains ancestor Suspense wrappers. In
prefetch-loading-shellmodeshouldSuppressLoadingBoundariesreturnsfalse, so the segment loop wraps the leaf loading shell with ancestor loading Suspense boundaries too. The fallbacks never fire (the shell content is synchronous), so this is almost certainly a visual no-op, but it is an untested change to prefetch-shell structure that isn't called out in the PR description. A focused assertion inapp-page-route-wiring.test.tsfor prefetch-shell + ancestor-loading would lock the behavior in. - Slot sub-routes inherit
ancestorLoadingTreePositionsverbatim from the parent (discoverSlotSubRoutes), the same aslayoutErrorPaths/layoutTreePositions. Consistent with existing conventions, just flagging it as the one place positions are copied rather than recomputed.
Verification done
vp test runon the 5 changed test files: 199 passed.vp checkon the 5 changed source files: format + lint + types clean.
Overall this looks correct and ready. The two minor items are optional follow-ups, not merge blockers.
|
Review posted to PR #2331. SummaryI reviewed PR #2331 (stream ancestor loading boundaries) and found it correct and ready to merge. The change is well-scoped. Verification:
Key findings (all correct):
Two non-blocking observations I noted in the review:
|
|
/bigbonk review for issues |
|
/bigbonk review for issues |
|
@james-elicx Bonk workflow was cancelled. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
2 similar comments
|
/bigbonk review for issues |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: stream ancestor loading boundaries
I independently verified the implementation, ran the targeted suite (229 tests across app-route-graph, app-page-route-wiring, app-page-dispatch, app-page-probe, app-route-module-loader, entry-templates — all green), ran vp check on the 5 changed source files (clean), and ran vp run vinext#build (passed). The change is correct, well-scoped, and ready to merge.
Verified correct
- Tree-position parity.
discoverAncestorLoadingswalkssegmentswithpath.join+findFileand assignsindex + 1positions, matchingdiscoverSegmentErrors/discoverLayouts/discoverTemplatesand aligning withcomputeLayoutTreePositions(depth = directory levels fromappDir, counting route groups). Ancestor positions therefore interleave correctly with layouts/errors inorderedTreePositions. - No leaf double-counting. Since
segments = dir.split("/"), the leaf is the last segment and itsloading.tsxis captured separately asloadingPathfromrouteDir. The ancestor loop stops atsegments.length - 1, and the root entry is gated onsegments.length > 0. Verified by hand for/(no ancestors),/dashboard(root only), and/dashboard/slow(root + dashboard). shouldProbeLayoutAtis geometrically aligned with the Suspense placement. A layout is skipped (and marked"d") only when an ancestor loading sits strictly above it (loadingTreePosition < layoutTreePosition). This exactly matches the rendering: ancestor loadings wrap their segment subtree (line 1089), while a layout at the same position as the leaf loading renders synchronously above the route-level Suspense boundary (line 962) and is correctly still probed. The?? 0/ optional-chaining defaults fail safe to "always probe".- Page-probe gating is a strict superset.
hasActiveLoadingBoundarynow addsroute.ancestorLoadings?.some((l) => l?.default)to the priorroute.loading?.defaultcondition, andprobePageSpecialErrorfolds the suppress check into that variable. Behavior-preserving for the no-ancestor case. The newdoes not reuse ancestor-loading-boundary RSCtest proves ancestor loadings skip the eager page probe and prevent dynamic-RSC reuse. - Manifest / lazy-hydration / slot inheritance mirror the existing
errorPathspattern (placeholdermoduleArray+__loadAncestorLoadings+ parallelancestorLoadingTreePositions), hydrate positionally inensureAppRouteModulesLoaded, and inherit verbatim to slot sub-routes — index alignment holds because both arrays derive from the same orderedancestorLoadingEntries. - Prefetch shell uses the rootmost boundary.
prefetchLoadingComponentsorts ancestor loadings by tree position and falls back to the route loading, matching Next.js's topmost-applicable-loading shell. Covered by the newuses the rootmost loading boundary for loading-shell prefetchestest.
Minor, non-blocking observations
- Manifest default inconsistency (cosmetic).
ancestorLoadingTreePositionsdefaults to[]while the siblingerrorTreePositionsdefaults tonull. Functionally identical for all consumers (?.some(...)/moduleArray(0)), just worth normalizing for consistency. - Prefetch-shell gains ancestor Suspense wrappers. Because
shouldSuppressLoadingBoundariesreturnsfalseforprefetch-loading-shell, the segment loop wraps ancestor segments (and their slots) in Suspense even thoughelements[pageId]isnulland the shell content is synchronous. A confirmed structural no-op — the fallbacks never fire — but anapp-page-route-wiring.test.tsassertion for prefetch-shell + ancestor-loading would lock it in.
Verification
vp test runon the 5 changed test files: 229 passed.vp checkon the 5 changed source files: format + lint + types clean.vp run vinext#build: passed.
No correctness issues found. The two items above are optional follow-ups, not merge blockers.
|
Review posted to PR #2331. SummaryI reviewed PR #2331 (stream ancestor loading boundaries) and found it correct and ready to merge. Verification performed:
Key findings (all correct):
Two non-blocking observations:
No correctness issues found. |
Summary
loading.tsxmodules in App Router route metadataNext.js parity
Targets the remaining non-cache initial-HTML loading failures from Next.js v16.2.6
test/e2e/app-dir/app/index.test.ts:should render loading.js in initial html for slow layoutshould render loading.js in initial html for slow layout and pageBootstrap script count remains a separate unowned gap and is intentionally out of scope.
Research
create-component-tree.tsx@vitejs/plugin-rsc0.5.27 streams correctly placed React Suspense fallbacks without plugin changesValidation
vp checkon 18 changed filestests/e2e/app-router/loading.spec.tscases passed with one workervp run vinext#buildpassed