Skip to content

[pull] canary from vercel:canary - #1333

Merged
pull[bot] merged 15 commits into
code:canaryfrom
vercel:canary
Aug 24, 2026
Merged

[pull] canary from vercel:canary#1333
pull[bot] merged 15 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Aug 24, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

unstubbable and others added 15 commits August 24, 2026 10:41
We are about to reduce the number of these entries, and each of those
changes should arrive with a diff that shows which ones it removes or
merges. That is what these fixtures record. The existing behavioral
suites stay responsible for regressions, and they do detect a routing
change of this kind, e.g. a build that dropped the `.rsc` entries of
fallback shells would fail a root param case in
`segment-cache/prefetch-app-shell`. These snapshots add the part those
suites may not distinguish, since an entry that collapses into a less
specific match can still serve a response that renders the same content.

One fixture covers Cache Components with root params, the shape that
grows with the number of root param combinations. The other covers what
does not depend on Cache Components: the `.rsc` and plain entry pair
that every dynamic app page receives, a route handler, pages that share
one shape and differ in a static last segment, and a `fallback: false`
pages router route whose plain entry carries a preview bypass condition
that its `.rsc` sibling does not. It also holds a proxy next to static
pages router pages, which adds one entry per page and is the only case
here whose count grows with the number of pages rather than with the
number of route shapes. A third test builds the first fixture again
under a base path and asserts the prefix on every entry.

The projection stays narrow so that unrelated build output leaves it
alone. Both fixtures pin `cacheComponents` and `generateBuildId`,
because CI would otherwise vary them.
For a dynamic app page that has a fallback shell, say `/[lang]/[slug]`
with a shell for the root param value `de`, the build emits three routes
to an adapter. Simplified, with the request pattern on the left and the
artifact it resolves to on the right:

```diff
- /de/<slug>.segments/$d$lang/$d$slug<seg>   ->  /de/[slug].segments/$d$lang/$d$slug<seg>
  /de/<slug><.rsc|.segments/*.segment.rsc>   ->  /de/[slug]<matched suffix>
  /de/<slug>                                 ->  /de/[slug]
```

This change removes the first one. The second already covers it: its
suffix group accepts `.segments/<path>.segment.rsc` as well as `.rsc`,
and it copies the matched suffix into the destination, so a per-segment
request resolves to the same artifact either way.

That second route is also the only one that ever answered `_tree` and
`_full` requests, because a per-segment route pins one literal segment
path in its regex. A `_tree` prefetch for `/de/<slug>` resolves to
`/de/[slug].segments/_tree.segment.rsc`, which the per-segment route
cannot produce. So this removes a duplicate, not a mechanism.

Only fallback shells emit per-segment routes, because their artifacts
sit under an unresolved param and need a rewrite to reach. Segment
artifacts for a concrete prerendered path need none, since a request for
that path matches them directly. Apps with many shells therefore lose
close to a third of their routes. We measured an in-progress feature
branch of the v0 chat app, which enumerates precomputed flag, locale and
device permutations in `generateStaticParams` for a top-level dynamic
segment and so multiplies every route below it. Building that branch
before and after the change removes 32% of its routes, adds none, and
changes nothing else once the build ID is normalized.

This also adds `experimental.collapseAdapterRoutes`. It defaults to
`true`, and it controls this collapse together with the ones that follow
in this stack. A build that sets it to `false` emits the same route
table as a build without any change in this stack.

`prefetchSegmentDataRoutes` stays in `routes-manifest.json`. A build
that does not use the adapter reads the field from that manifest and
derives the same routes from it. This change therefore leaves that path
alone.

**Verified upstack with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32540912007).**
…97726)

A dynamic app page in an app with an app directory needs two routes
today. One matches a request for the page, and one matches a request for
its `.rsc` payload or a per-segment prefetch. Simplified, with the
request pattern on the left and the artifact it resolves to on the
right:

```diff
- /de/<slug><.rsc|.segments/*.segment.rsc>    ->  /de/[slug]<matched suffix>
- /de/<slug>                                  ->  /de/[slug]
+ /de/<slug><.rsc|.segments/*.segment.rsc|>   ->  /de/[slug]<matched suffix>
```

One entry now serves both, because the suffix group gains an empty final
alternative. The group therefore always matches. It captures an empty
string for a request that carries no suffix, and the destination copies
whatever it captured, so a plain request resolves to the page and a
suffixed request resolves to the artifact it asks for. The parameter is
matched lazily, so a request for `/de/other.rsc` still prefers the
shorter parameter and the `.rsc` suffix over a parameter that swallows
the suffix.

The empty alternative is deliberate, and an optional group would not be
equivalent. A consumer of these entries can resolve the placeholders in
a destination from the match result rather than from the pattern. A
group that does not take part in the match is then absent from that
result, and the literal text `$rscSuffix` survives into the destination.
A group that always takes part avoids depending on how any one consumer
treats an absent key, and the cost of getting it wrong is a 404 on every
plain navigation to every dynamic route.

One case keeps two entries. A pages router route with `fallback: false`
requires the preview cookies on the request for the page, and not on the
request for its `.rsc` payload. An entry carries one set of conditions
and cannot express that difference, so the merge applies only when both
forms agree on their conditions.

Every app that has an app directory therefore halves the entries for its
dynamic routes, whether or not it uses Cache Components. We measured the
same in-progress feature branch of the v0 chat app as the previous
change in this stack, which enumerates precomputed flag, locale and
device permutations in `generateStaticParams` for a top-level dynamic
segment and so multiplies every route below it. Every dynamic entry on
that branch merges. Building it before and after the change removes 48%
of the routes that the previous change left, replaces each merged pair
with a single entry, and changes nothing else once the build ID is
normalized. Across both changes that branch loses 65% of its routes.

This collapse follows `experimental.collapseAdapterRoutes`, which the
previous change in this stack added. A build that sets it to `false`
emits a separate entry for each form.

**Verified with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32540912007).**
…7728)

The existing fixtures take a single root param, so they do not cover the
shapes that the fallback shell entries can take. This adds fixtures and
suites for three more shapes, and pins the entries that each one
produces.

The first has a root layout that takes two root params, and returns
three combinations of them rather than the full product of four. An
entry that held each root param position separately would also match
`sparse/de`, a combination the build never prerendered, and a request
for it would resolve to an output that does not exist. One combination
is `acme.one-two,three`, which carries the characters a regex treats as
special, and the snapshot shows the build escaping them.

The second builds that same app with a single combination, so each
source page has one fallback shell.

The third has no root params at all. Its root layout sits above the
dynamic segments, so `team` and `locale` are ordinary dynamic params
that `generateStaticParams` enumerates on a nested layout. The build
then produces two shapes of entry for one source page: entries that
resolve both params, such as `/sparse/en/posts/[id]`, and entries that
resolve only the first, such as `/sparse/[locale]/posts/[id]`. The order
of those entries carries the behavior, because a request for
`/sparse/en/posts/1` has to reach the output that resolves both params
rather than the one that resolves only the first. The snapshot pins that
order.

Two comments on the Cache Components suite were wrong, and this corrects
them. That fixture takes one root param with two values, and not two
root params. Its entries also no longer come in pairs, because one entry
serves the request for the page, the request for its `.rsc` payload, and
a per-segment prefetch.
A fallback shell repeats the whole path of its source page and resolves
the leading params to concrete values. Take `/[team]/[locale]/[slug]`,
where the build prerenders three combinations of the two leading params.
It emits one entry per combination, and the entries differ only in that
leading part of the path:

```diff
- /acme/en/<slug><suffix>    ->  /acme/en/[slug]<matched suffix>
- /acme/de/<slug><suffix>    ->  /acme/de/[slug]<matched suffix>
- /globex/en/<slug><suffix>  ->  /globex/en/[slug]<matched suffix>
+ /(?<shellPrefix>acme/en|acme/de|globex/en)/<slug><suffix>  ->  /$shellPrefix/[slug]<matched suffix>
```

One entry now serves them all. Its pattern lists the leading part of
each shell path as an alternative, and its destination copies whichever
one matched.

Those alternatives are complete, and that matters. A pattern that
offered a choice per param instead, `(acme|globex)/(en|de)`, would also
match `globex/de`. The build never prerendered that combination, so a
request for it would resolve to an output that does not exist, and it
would then fall through to whichever route claims the rewritten path.

An entry serves neighbours in the manifest, and only those. It takes the
position of the first shell that it replaces, so every replaced shell
keeps its place relative to the routes around it. Any other route
between two shells ends the run, because an entry that reached across it
would move ahead of a route that a request matches first. The shells of
a run also have to agree on `fallback: false`, because an entry carries
one set of conditions.

A source page can therefore hold several runs, and a shell can belong to
none. That happens when the build resolves a different number of params
for neighbouring shells, which leaves them with different paths after
the resolved part.

This removes the multiplier that the number of prerendered combinations
applies to every route below the resolved params. We measured the same
in-progress feature branch of the v0 chat app as the previous changes in
this stack. Building it before and after this change removes 87% of the
routes that those changes left. Across the stack that branch loses 95%
of its routes.

This collapse follows `experimental.collapseAdapterRoutes`, which an
earlier change in this stack added. A build that sets it to `false`
emits one entry per shell.

**Verified with a [full deploy test
run](https://github.com/vercel/next.js/actions/runs/32577936779).**
`experimental.collapseAdapterRoutes` now defaults to `false`, so a build
keeps one entry per route unless the project opts in. The changes below
it in this stack stay as they are, and a project that sets the option to
`true` gets the route table that they produce.

The default starts off so that we can dogfood the behavior on selected
apps before every build gets it. The default moves back to `true` once
enough apps have run with it.

The four fixtures under `test/production/app-dir/adapter-dynamic-routes`
set the option, because their snapshots pin the collapsed table. A build
of the Cache Components fixture without the option emits the 27 entries
that the first commit in this stack recorded, which is what the option
now turns off.
**View diff without whitespaces**

Fix remaining cases of #97666

- Mostly replacing async blocks with async closures
- And second commit: some more making closures sync where possible
…96536)

While writing an adapter against `16.3.0-canary.107` I noticed the
adapter docs are missing two fields that the shipped `NextAdapter` types
already have.

**`assetsHashes`** — every `PAGES` / `PAGES_API` / `APP_PAGE` /
`APP_ROUTE` / `MIDDLEWARE` output carries it right next to `assets`, and
it's declared with a doc comment in `build-complete.d.ts`, but none of
the five shapes in Output Types mention it.

**`routing.middlewareMatchers`** — dumping the `routing` object from a
real `onBuildComplete` call gives:

```
afterFiles, beforeFiles, beforeMiddleware, dynamicRoutes, fallback,
middlewareMatchers, onMatch, rsc, shouldNormalizeNextData
```

but the docs list eight of those nine, everywhere the interface appears:
the Creating an Adapter snippet (which says "The interface is defined as
follows"), the API Reference parameter list, and Routing Information.
This one feels worth fixing soon — an adapter that does its own request
matching from the documented fields alone has no way to decide when
middleware should run. My guess for why nobody has hit it: if you pass
`routes: routing` wholesale into `resolveRoutes` from `@next/routing`,
everything works without ever looking at the field.

The wording I added comes from the doc comments in the shipped types,
not my own descriptions. `middlewareMatchers` is inserted where the type
puts it (right after `beforeMiddleware`).

For what it's worth: I checked the rest of the section against the same
build while I was at it — the `output: 'export'` behavior, the prerender
classification fields, `pprChain.headers`, the fallback fields, the
immutable-assets flow, and the `@next/routing` params/result — and
everything else matched the docs. These two were the only gaps I found.
## Summary

The basePath external-navigation test depended on loading Google and
recognizing its DOM before exercising browser history. That made the
test sensitive to public network availability and third-party page
changes; the reported failure was confirmed in the Safari/WebKit CI run
on the initial attempt and both retries.

For local and CI test-server runs, this replaces the public dependency
with a minimal page served from a separate local origin, preserving the
cross-origin, full-document navigation being tested. Deploy mode remains
enabled and uses a deploy-accessible external target because deployed
browsers cannot reach the test runner. Both paths now synchronize on
browser origin and exact restored-page state instead of third-party DOM
or a fixed sleep.

## Verification

- Focused production/start-mode test passed three consecutive times in
Firefox
- Focused production/start-mode test passed in Chromium
- Prettier and ESLint passed for the changed files
- `pnpm build-all`
- Not run: Safari/WebKit locally (Playwright’s unsupported Amazon Linux
fallback requires incompatible Ubuntu system libraries; the Safari CI
job is the definitive validation)
- Not run: deploy-mode execution locally (deployment credentials
unavailable; the deploy path remains enabled for CI)

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
### What?

Separate trace-graph module path collection from module content hashing.
Exclusion filtering now requests only cached module identifiers, while
full NFT data still layers content hashes on top when they are required.

### Why?

Applying output-file-tracing exclusion globs needs module paths but not
content hashes. Keeping these computations separate avoids requesting
full-graph hashes solely to decide which modules should be skipped.

This is an inspection experiment, not a demonstrated performance
improvement. A seven-sample release A/B left the median compilation
phase unchanged at 2700 ms.

### How?

A private identifier-only graph task owns the existing DFS traversal.
`traced_module_data_for_graph` reuses those identifiers and computes
hashes for its full result. `traced_modules_for_entries` drops the
unused hash-salt input and consumes only identifiers for glob matching.

### Verification

- `cargo fmt -- --check`
- `cargo check -p next-api`
- `next-server.js.nft.json`: byte-identical SHA-256
`da4c5a494efbbb38eb966066115de96adb5d0ec693fba17bbed669c99cafae43`
- `next-minimal-server.js.nft.json`: byte-identical SHA-256
`7c58d40dc2ecbe6ddd26dd0c602467d80f1a8ea3320e2c6e8e5f4d6be14156cd`
- Not run: clippy, Rust test suite, or integration tests (inspection
experiment)

<!-- NEXT_JS_LLM -->

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Some cleanup, this was never used:
- `config` from `getStaticInfoIncludingLayouts`
- `getServerSideProps` and `getStaticProps` from pages router
### What?

Exposes the `cacheHandlers` types (`CacheHandler`, `CacheEntry`) as
types-only exports from `next/cache`, so handlers can be checked against
the real interface:

```ts
import type { CacheHandler } from 'next/cache'
```

### Why?

Custom cache handler authors currently import `CacheHandler` and
`CacheEntry` from `next/dist/server/lib/cache-handlers/types` (an
internal path that can move between versions) or hand-copy the
interfaces, which drift silently across releases.

### Motivation

I'm working on a custom community cache handler and it'd be great to
import the types directly and keep testing against the source of truth,
rather than maintaining a hand-copied mirror that has to be re-checked
on every Next.js release.

Raised in discussion #96356.

### How?

- Adds a type-only `export type { CacheHandler, CacheEntry }` to
`packages/next/cache.d.ts`, re-exported from
`./dist/server/lib/cache-handlers/types`. No new subpath or
`package.json` `"files"` entries needed since `next/cache` already
ships.
- Docs: `cacheHandlers.mdx` now shows the public `next/cache` import
instead of the GitHub source links.
- Tests: a `satisfies CacheHandler` / `satisfies CacheEntry` fixture in
the `typescript-basic` typechecking suite (runs `tsc` against the
installed package), and the `use-cache-custom-handler` e2e fixture's
JSDoc now uses the public import.

(Originally proposed as a separate `next/cache-handlers` types-only
subpath, following the `next/types` pattern moved the export into
`next/cache` per review.)

Related: #96356

closes #97781 (only created to run deploy tests)
### What?

Reduce the binary-size cost of TurboMalloc's allocation instrumentation
while preserving its hot-path performance and accounting semantics.

This adds a focused Criterion benchmark for TurboMalloc's raw allocation
paths and moves only the uncommon global counter refill/spill operations
behind cold, non-inlined helpers. Thread-local allocation counters,
buffer checks, and common buffer adjustments remain inline at allocation
sites.

### Why?

Rust gives global allocator methods special inlining treatment.
TurboMalloc's thread-local accounting was consequently duplicated across
allocation and deallocation call sites in `next-swc`, contributing
several megabytes of machine code.

Moving the whole accounting path out of line recovered most of that
space but made small allocations slower. Splitting only the rare atomic
paths provides a smaller size reduction without that hot-path cost.

### How?

The common path remains explicitly inlineable through `add`, `remove`,
`update`, and the thread-local access helper. When the local buffer must
be refilled or spilled, it calls a `#[cold] #[inline(never)]` helper
that performs the global relaxed atomic update.

The arithmetic, thresholds, target buffer values, counter increments,
and atomic ordering are unchanged. The counter test now also covers
realloc growth/spill and shrink/flush behavior.

### Benchmark results

Linux x86_64 Intel Xeon, Rust 1.99.0-nightly / LLVM 22.1.8. Native
artifacts used the repository's production release command. Direct
application measurements used clean `bench/heavy-npm-deps` `next build
--turbopack` runs, with 8 samples per arm and rotated/interleaved
ordering.

#### Binary size

| alternative | raw artifact delta | `.text` delta |
|---|---:|---:|
| No instrumentation | −6,260,168 B (−4.23%) | −5,466,304 B (−7.69%) |
| `inline(never)` on `add` / `remove` / `update` | −4,338,856 B (−2.93%)
| −4,342,464 B (−6.11%) |
| `inline(never)` on whole `GlobalAlloc` methods | −4,153,792 B (−2.80%)
| −3,714,688 B (−5.23%) |
| **Cold atomic refill/spill helpers (this PR)** | **−502,864 B
(−0.34%)** | **−732,736 B (−1.03%)** |

The current artifact was approximately 148.1 MB raw with 71.1 MB of
`.text`. The retained split recovers 8% of the raw and 13% of the
`.text` saving from removing instrumentation entirely.

#### Direct `next build --turbopack`

| alternative | mean effect vs current | bootstrap 95% CI | permutation
p |
|---|---:|---:|---:|
| No instrumentation | −0.70% | [−2.32%, +1.12%] | 0.466 |
| `inline(never)` on `add` / `remove` / `update` | **+3.53%** |
**[+0.65%, +6.43%]** | 0.0448 |
| `inline(never)` on whole `GlobalAlloc` methods | +1.02% | [−2.03%,
+4.44%] | 0.586 |
| **Cold atomic refill/spill helpers (this PR)** | **−0.18%** |
**[−3.44%, +3.61%]** | **0.929** |

No application-level difference was detected for the retained split or
the whole-method placement. The helper-level noinline placement showed
evidence of a modest regression.

#### Focused allocator Criterion benchmark

Percent changes are lower-is-better. Where two figures are shown, they
are independent runs in reversed order.

| alternative | alloc + dealloc | alloc + realloc + dealloc |
|---|---:|---:|
| No instrumentation | −43.4% | −36.1% |
| `inline(never)` on `add` / `remove` / `update` | +5.5% / +7.5% | +3.0%
/ −8.1% (inconclusive) |
| `inline(never)` on whole `GlobalAlloc` methods | **+19.4% / +21.4%** |
**+2.9% / +6.4%** |
| **Cold atomic refill/spill helpers (this PR)** | **−4.0% / −3.0%** |
**−2.8% / −5.4%** |

The full 28-cell turbo-tasks overhead suite was also run twice in
reversed order. Large process-level effects changed magnitude or sign
between runs, so no conclusion is drawn from that suite.

### Verification

- `cargo fmt -- --check`
- `cargo clippy -p turbo-tasks-malloc --all-targets -- -D warnings -A
deprecated`
- `cargo test -p turbo-tasks-malloc`
- `cargo check -p turbo-tasks-malloc --all-targets`
- `cargo bench -p turbo-tasks-malloc --bench allocation -- --test`
- Release `next-swc` builds and Node smoke-loads for every measured arm

<!-- NEXT_JS_LLM -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
### What?

Collapses three duplicated crate dependencies, so each is built once
instead of twice. **−150 KiB** of code

Manifests and `Cargo.lock` only — no source changes, and none were
needed.

**`regress` → 0.11.1** (−133 KiB, the bulk of the win)

**`wat` and `wasmparser` → the 239 family** (−13 KiB)

**`base64` → one workspace declaration on 0.22** (−1.2 KiB)
@pull pull Bot locked and limited conversation to collaborators Aug 24, 2026
@pull pull Bot added the ⤵️ pull label Aug 24, 2026
@pull
pull Bot merged commit 88483bd into code:canary Aug 24, 2026
1 of 4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants