Skip to content

feat(app-router): track dynamic usage in a per-request sticky latch - #3454

Draft
james-elicx wants to merge 6 commits into
isr-query/03-never-cache-controlfrom
isr-query/04-dynamic-latch
Draft

james-elicx wants to merge 6 commits into
isr-query/03-never-cache-controlfrom
isr-query/04-dynamic-latch

Conversation

@james-elicx

@james-elicx james-elicx commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Stacked on #3453. First part of plan PR 1 (cache-candidate SSR mode), which fixes a query-bearing miss poisoning the shared ISR key.

Problem

In cache-candidate mode, SSR useSearchParams() will wait on a per-request gate. The gate opens with real values as soon as the render turns uncacheable, and bails out to client rendering once the render settles. That needs a signal for "this render has used a dynamic API at any point", and the existing flag can't provide it:

  • consumeDynamicUsage resets dynamicUsageDetected at the shell, so a gate issued after the shell would miss earlier usage.
  • Isolated scopes don't propagate their marks to it. That covers the layout probe, runWithIsolatedDynamicUsage, and unified-state mutations that reset the flag in a child clone.

Change

  • VinextHeadersShimState.renderDynamicLatch is one object per request. It's created with the request's headers context (runWithHeadersContext, setHeadersContext, createRequestContext) and never replaced. Every child scope shallow-copies state, so they all share the same object; the clone comment now lists it among the fields that must stay shared.
  • markDynamicUsage sets the latch and notifies waiters. Nothing clears it. force-static never sets it, because markDynamicUsage returns early there.
  • isRenderDynamicLatched() and onRenderDynamicLatched(listener) expose it. The gates in the next PR use them.

Nothing reads the latch yet, so there's no behaviour change.

Tests

tests/render-dynamic-latch.test.ts covers the plan's latch test:

  • usage inside runWithUnifiedStateMutation, the layout probe (runLayoutProbe) and the non-unified runWithIsolatedDynamicUsage is visible later, even after the flag is consumed;
  • a waiter is notified exactly once, and not after it unsubscribes;
  • force-static never latches;
  • every request starts unset.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues
Please review exact head 13fc32f313f950e3e24a09add247318dc6d20223 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@pkg-pr-new

pkg-pr-new Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@vinext/cloudflare

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

create-vinext-app

npm i https://pkg.pr.new/create-vinext-app@3454

@vinext/types

npm i https://pkg.pr.new/@vinext/types@3454

vinext

npm i https://pkg.pr.new/vinext@3454

@cloudflare/workers-response-store

npm i https://pkg.pr.new/@cloudflare/workers-response-store@3454

commit: 567f094

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 13fc32f to ced765b Compare September 25, 2026 00:13
@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 567f094 against base efd7761 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 142.3 KB 142.3 KB ⚫ +0.0%
Client entry size (gzip) vinext 129.7 KB 129.7 KB ⚫ +0.0%
Dev server cold start vinext 2.53 s 2.52 s ⚫ -0.5%
Production build time vinext 2.82 s 2.80 s ⚫ -0.8%
RSC entry closure size (gzip) vinext 129.9 KB 130.0 KB ⚫ +0.1%
Server bundle size (gzip) vinext 228.2 KB 228.4 KB ⚫ +0.1%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 2d6ab8e4fab283723f31cb627401dbf232bd8ce4 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
response-store-demo preview production
workers-cache preview production
kv preview production
static-export preview production
web preview production

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 2d6ab8e to cf41f57 Compare September 25, 2026 01:58
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head cf41f57b57fd03861f50111ba46d547dcb27f669 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from cf41f57 to 3220ace Compare September 25, 2026 02:05
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 3220acef0e0794f8c0d7ab6506569bf876142198 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 3220ace to 463df6c Compare September 25, 2026 02:25
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 463df6c538ab068c97db822b76721e4eac77b30e without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 463df6c to 4b0f05c Compare September 25, 2026 02:32
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 4b0f05cbf87832a918544eef569c41bee5f7524e without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head 1403ab29e551286aa8eac1ad888c2e31b30aef9f without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor
  1. Low: headers.ts:225-228 propagates dynamicUsageDetected to pre-HMR probe parents but latches only the child. An existing parent latch remains false and its listeners never fire. Latch each propagated target and test an in-flight pre-HMR probe.

github run

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 1403ab2 to a67b296 Compare September 25, 2026 15:31
@james-elicx

Copy link
Copy Markdown
Member Author

Fixed the Low finding (headers.ts:225-228) in a67b296: markDynamicUsage now sets the latch on the current scope and on every probe parent the flag is propagated to (creating one where missing), and fires all their listeners once every flag is set. A shared latch still fires only once. The new test (an in-flight pre-HMR probe whose parent latch and listener must fire) fails without the fix.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

Please review exact head a67b296ff885820b6ee77bba2a80d7e4021eb173 without modifying or pushing the branch. Report all actionable findings within your 8-minute time limit.

@ask-bonk

ask-bonk Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from a67b296 to 73b1e53 Compare September 25, 2026 18:07
@james-elicx
james-elicx removed this pull request from stack #3471 September 25, 2026 18:50
@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 73b1e53 to 8b378cc Compare September 25, 2026 18:50
@james-elicx
james-elicx changed the base branch from isr-query/03-never-cache-control to isr-query/03b-intercept-config September 25, 2026 18:50
@james-elicx
james-elicx added this pull request to stack #3474 September 25, 2026 18:51
@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch 3 times, most recently from dbe1edb to 17a0984 Compare September 25, 2026 20:46
consumeDynamicUsage resets the dynamic flag at the shell, and isolated
scopes such as the layout probe don't propagate to it, so code running late
in a render can't tell whether the render already used a dynamic API. Add
one latch object per request, shared by every child scope, that
markDynamicUsage sets and nothing clears, with a way to wait for it.
… and throwing listeners

The headers fallback state persists on globalThis across HMR, so one created
before the latch existed has none; create it on first access. Listeners are
cleared before they run, so a throwing one skipped the rest forever and failed
the dynamic API call; notify each one and log its error instead.
…ld scopes

A state that predates the latch got one only on first access, so a child
scope cloned from it created its own and dynamic usage there never reached
the parent. Create the latch on the parent before every clone: in
runWithUnifiedStateMutation and in the fallback isolated-usage and
connection-probe scopes.
…rded dynamic usage

A state created before an HMR update has no latch. Creating it unset lost
dynamic usage the state had already recorded, so start it from the state's
dynamicUsageDetected flag. The headers shim and unified scopes now share one
ensureRenderDynamicLatch helper.
…-populated dynamic usage flag

The default latch was built before `...opts` applied an overridden
dynamicUsageDetected, so createRequestContext({ dynamicUsageDetected: true })
returned an unset latch that ensureRenderDynamicLatch would not reconcile.
Start it from the passed flag; an explicitly passed latch still wins.
@james-elicx
james-elicx removed this pull request from stack #3474 September 25, 2026 21:30
@james-elicx
james-elicx changed the base branch from isr-query/03b-intercept-config to isr-query/03-never-cache-control September 25, 2026 21:31
@james-elicx
james-elicx force-pushed the isr-query/04-dynamic-latch branch from 17a0984 to 567f094 Compare September 25, 2026 21:31
@james-elicx
james-elicx added this pull request to stack #3481 September 25, 2026 21:32
@james-elicx
james-elicx removed this pull request from stack #3481 September 25, 2026 22:13
@james-elicx
james-elicx added this pull request to stack #3482 September 25, 2026 22:14
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.

1 participant