Skip to content

fix(app-router): isolate parallel slot page CSS#2095

Open
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/parallel-slot-css-isolation
Open

fix(app-router): isolate parallel slot page CSS#2095
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/parallel-slot-css-isolation

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Overview

Area Detail
Goal Keep non-intercepting parallel-route page CSS out of unrelated production routes
Core change Emit slot.pagePath modules as lazy manifest loaders and hydrate them onto slot.page only when the request takes the page-render path
Boundary App Router production RSC manifest, route-module hydration, route-handler dispatch, and route-level global CSS chunks
Primary files app-rsc-manifest.ts, app-route-module-loader.ts, app-rsc-handler.ts, app-router-production-server.test.ts
Impact A parallel slot page that imports global CSS no longer contributes that CSS to unrelated pages through the shared RSC manifest chunk

Closes #1817.

Why

Parallel slot page modules are page modules in the active route tree. They should be loaded when the matched page route needs them, not when the generated RSC manifest module is evaluated for every route. PR #1738 fixed this for ordinary matched pages and intercepting pages, but slot.pagePath still used the eager import path.

Area Principle / invariant What this PR changes
CSS chunks Route-level global CSS belongs to the route tree that imports it Non-intercepting slot pages now use lazy import() loaders instead of eager import * as manifest imports
Runtime loading Rendering code reads synchronous slot.page modules Route entry hydration loads only the matched page and route-handler modules before dispatch; page-route hydration then loads parallel slot pages before segment config and rendering read the route
Route handlers A route handler should not evaluate unrelated UI slot pages before returning a Response handleAppRscRequest calls ensureRouteLoaded(route, { includeParallelSlotPages: false }) before the route-handler branch and performs full slot hydration only after the handler branch declines
Regression coverage CSS isolation and route-handler dispatch must stay separated Adds production CSS isolation coverage plus a route-handler regression test asserting a parallel slot page loader is not called for handler dispatch

What changed

Scenario Before After
@slot/page.tsx imports route-global CSS The slot page was statically imported into the generated manifest, so its CSS could join shared production CSS The slot page is emitted as page: null plus __loadPage, then hydrated only for matched page rendering
Matched page route with a slot page Slot page was already synchronously present Page-route hydration imports the slot page and assigns slot.page before downstream reads
Matched route handler with inherited slots A naive slot hydration path could evaluate slot pages before handler dispatch Pre-dispatch hydration loads only route entry modules, so the handler can dispatch without evaluating slot UI pages
Unrelated production route Could link CSS from a parallel slot page it never rendered Does not link the slot CSS
Maintainer review path
  1. packages/vinext/src/entries/app-rsc-manifest.ts: verify slot.pagePath now uses getLazyLoaderVar and emits __loadPage.
  2. packages/vinext/src/server/app-route-module-loader.ts: verify entry hydration and full page-route hydration are distinct, with includeParallelSlotPages: false skipping slot page loaders.
  3. packages/vinext/src/server/app-rsc-handler.ts: verify route handlers dispatch after entry-only hydration and before full slot page hydration.
  4. tests/app-rsc-handler.test.ts: verify a route handler with an inherited slot does not call the slot page loader.
  5. tests/app-router-production-server.test.ts plus the parallel-slot-css-isolation fixture: verify the CSS regression is covered at the production stylesheet boundary.
Validation
  • vp test run tests/app-route-module-loader.test.ts tests/app-rsc-handler.test.ts -t "ensureAppRouteModulesLoaded|parallel slot pages before route handler"
  • vp test run tests/app-route-module-loader.test.ts tests/app-rsc-handler.test.ts tests/entry-templates.test.ts tests/app-router-production-server.test.ts -t "ensureAppRouteModulesLoaded|parallel slot pages before route handler|route module imports|non-intercepting parallel slot pages"
  • vp test run tests/app-rsc-handler.test.ts
  • vp test run tests/app-router-production-server.test.ts
  • vp check --fix
  • vp check
  • git diff --check
  • Commit hook: staged checks, full check, staged unit/integration tests, knip
Risk / compatibility
  • Public API: no API change.
  • Runtime: matched App Router page routes now load non-intercepting parallel slot pages during page-route hydration. Route handlers intentionally do not load those slot page modules before dispatch.
  • CSS/build output: intended chunking change for slot page modules. Slot defaults, layouts, loading, and error boundaries remain eager.
  • Error behavior: rejected route or slot page imports use the existing route-loader rejection path, so failures are not cached and can retry on the next request.
Non-goals

References

Reference Why it matters
Issue #1817 Tracks the remaining slot.pagePath gap from #1738
PR #1738 Prior fix for matched page and intercepting page CSS isolation
Next.js parallel-route CSS navigation test Confirms CSS is expected to load with parallel-route navigation, not via unrelated routes
Next.js app loader parallel route tree construction Shows parallel routes are resolved as route-tree entries rather than one shared eager page import

@pkg-pr-new

pkg-pr-new Bot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2095
npm i https://pkg.pr.new/vinext@2095

commit: 6928f0f

Parallel route slot pages were still emitted as eager manifest imports, so a slot page that imported route-level global CSS could be bundled into the shared RSC manifest chunk and leak styles into unrelated production routes.

The manifest now emits non-intercepting slot pages as lazy loaders, and the App Router route-module hydrator resolves those loaders onto slot.page before segment config or rendering reads the route tree.

Regression coverage adds a production fixture for a non-intercepting @panel/page.tsx with global CSS and verifies unrelated routes do not link that CSS while the slot route still does.
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/parallel-slot-css-isolation branch from 7b25e3a to f46b3c7 Compare June 17, 2026 04:35
…n requests

The generated handleServerActionRequest wrapper is invoked for every App
Router request, not only real server actions. It now returns early when no
action id is present, and entry-loads only the matched route (page + route
handler) for actual action requests, leaving parallel slot page hydration to
the page render path.

This closes the remaining path where a plain GET to a route handler with
inherited parallel slots could evaluate slot page modules before dispatch.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

Fixed the server-action preflight path in the generated RSC entry.

Changes (pushed to 6928f0ff):

  • packages/vinext/src/entries/app-rsc-entry.ts: handleServerActionRequest now returns null immediately when actionId is absent, and entry-loads the matched route with includeParallelSlotPages: false for actual action requests. This prevents parallel slot page modules from being evaluated before a route-handler dispatch branch runs.
  • tests/app-rsc-handler.test.ts: added a regression test that models the generated wrapper behavior and asserts that a plain GET to a route handler with inherited parallel slots does not load the slot page module.
  • tests/app-router-next-config-codegen.test.ts: added a codegen assertion verifying the generated wrapper contains the early-return guard and the includeParallelSlotPages: false option.

Verification:

  • vp test run tests/app-rsc-handler.test.ts tests/app-router-next-config-codegen.test.ts tests/app-router-isr-codegen.test.ts — 115 tests passed.
  • vp test run tests/app-route-module-loader.test.ts tests/app-rsc-handler.test.ts tests/entry-templates.test.ts tests/app-router-production-server.test.ts -t "ensureAppRouteModulesLoaded|parallel slot pages before route handler|route module imports|non-intercepting parallel slot pages" — 14 tests passed.
  • vp check — formatting, lint, and type checks passed.
  • No merge conflicts with upstream/main.

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review June 17, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Track parallel-route page CSS isolation in App Router manifest

1 participant