Vinext 1.3: fewer requests per page, faster cold starts, and Worker Previews
James Anderson9 min read
To upgrade:
npm install vinext@1.3.0 @vinext/cloudflare@1.2.0
Three changes may need your attention when you upgrade:
- Invalid
redirects,rewritesandheadersrules now fail config loading. Vinext used to skip them at runtime. Aheaders()source written as a glob, such as/api/*, is one example. See next.config route matching. - Client JS and CSS files have hash-only names.
_next/static/chunks/AdminAccountMenu-<hash>.jsis now_next/static/chunks/<hash>.js. If something in your setup depends on those names, you can set your own. - Client components are grouped into one chunk per set of routes in App Router production builds.
disableNextLikeChunking: trueturns this off. See Fewer requests per page.
Fewer requests per page
App Router production builds used to load every "use client" module through its own dynamic import(). Each client component became its own chunk, with its own shared chunks. On a prototype of a large content site, that was about 82 JS requests per page. Next.js with Turbopack serves the same prototype with 18–28.
Vinext now groups the client components that the same set of routes reach into one chunk. This mirrors Next.js's per-route client entries:
| App | JS requests per page | CSS requests per page | JS gzip per page | 4G LCP |
|---|---|---|---|---|
| Content site prototype, Vinext 1.2 | 81.8 | 25.8 | 490.1 KiB | 957 ms |
| Content site prototype, Vinext 1.3 | 25 (−69%) | 15 (−42%) | 469.7 KiB (−4%) | 900 ms (−6%) |
| Content site prototype, Next.js 16.2.6 | 28 | 5 | 836.8 KiB | 852 ms |
"4G" is 150 ms RTT, 5 Mbit/s and a 4× CPU throttle.
A few rules keep the groups from loading code a page doesn't need:
- Client components that a route reaches through
next/dynamicorReact.lazystay in their own lazy chunks. - A component is only grouped if its own code is 100 KiB or less. Heavier ones keep their own chunk, so a large component a route can render but rarely does isn't loaded on every visit. Without this cap, hydration on 4G got about 5% slower on the prototype.
- Intercepting routes,
global-errorandglobal-not-foundget their own groups, so a modal isn't loaded with every page. - A group's name comes from its members, so adding a route that reaches the same components doesn't rename it.
Grouping turns itself off if your client build already sets its own chunk groups (codeSplitting.groups, advancedChunks or manualChunks), from your config or another plugin. You can also turn it off with vinext({ disableNextLikeChunking: true }), which loads each client component from its own chunk as before.
Working out the groups needs an estimate of each component's code size. Three follow-up PRs made that estimate faster, so builds take about as long as before: the prototype builds in 9.2 s with grouping and 9.1 s without.
Faster cold starts
On a cold server, the first App Router SSR render loaded the client code of every page in the app before it rendered anything, and requests that arrived in the meantime waited behind it. @jgeurts measured 7.7 s for the first /login on a production app with about 250 client components, at 1 CPU.
Each render now waits only for the client components of the route it renders. The list is worked out at build time from the RSC module graph, using the same route walk as grouping. On a test app with 50 routes and 252 client components:
| Vinext 1.2 | Vinext 1.3 | |
|---|---|---|
First GET /login | 228 ms | 42 ms |
First GET of a 5-component page | 234 ms | 48 ms |
| Memory after the first request | 210 MB | 102 MB |
Warm requests are unchanged.
The App Router browser entry also used to inline the whole route manifest, which grew with every route in the app. @williamrogersdev reported this. The browser now loads a trimmed manifest from its own chunk, alongside hydration.
Build output
Rolldown names a chunk after its first module, so client file names published component and file names from your source. @caumeira found that this can also break pages: the EasyList Cookie List filter /consent-banner- blocks the chunk for a consent-banner.tsx client component, and the App Router page then fails to hydrate.
Client JS is now _next/static/chunks/[hash].js, and CSS is _next/static/css/[hash].css, as in Next.js. Media files keep their names, as Next.js does.
To use other names, set entryFileNames, chunkFileNames or assetFileNames in build.rolldownOptions.output or environments.client.build.rolldownOptions.output. Vinext used to override these, as @DaleyKD reported. It now only fills in the names you leave unset, and warns about patterns it can't serve correctly, such as names without [hash]. Build output file names has the rules.
Two builds of the same source now also produce the same client chunk and CSS file names. @quantizor reported that most client chunks were renamed on every deploy, even with deploymentId pinned. That made unchanged chunks miss the CDN and browser caches, and left tabs opened before a deploy unable to load them. The build ID, the RSC build identity and the RSC compatibility ID no longer reach client JS. The browser reads the values it needs from the page instead, as Next.js does. The key that names "use cache" exports is derived from the Server Actions encryption key, as in Next.js, instead of being random per build.
Less of your app in the client
Every App Router page shipped the path of every route.ts handler in the app, in two places. Pages Router builds did the same for every pages/api route. That published the app's whole API surface, internal endpoints included. @dppeak reported it, and @tamujin fixed it for App Router builds.
- App-only builds ship no route handler paths, except where the client router needs one to avoid briefly rendering the wrong page's loading state.
- Pages Router builds ship no API route paths.
- Builds with both
app/andpages/ship the handler patterns they need with every static segment replaced by a short hash. As with Next.js's client router filter, the list can't be read, but a guessed path can be checked against it.
next.config route matching
redirects, rewrites and headers sources were matched by Vinext's own parser, which differed from Next.js in many cases. Some headers() sources from the Next.js docs never matched, so headers such as X-Frame-Options, CSP and HSTS were left off responses Next.js would have added them to. All three now compile with path-to-regexp, as in Next.js 16. Other fixes in the same area:
- Locale-prefixed redirects with params, such as
/:locale(en|sv)?/settings/:path*, now fire under non-default locales. headers()substitutes matched params into header keys and values. Avalue: ":slug"used to be sent as the literal text:slug.- With
i18n, aheaders()rule matches once, in its locale-prefixed form. Additive headers such asSet-Cookiewere sent twice on default-locale requests. - Config and middleware headers apply to external rewrites, and
Set-Cookieheaders fromnext.configare ordered as in Next.js. - Regexes in sources,
has/missingvalues and middleware matchers run as written, as in Next.js. Vinext used to check them for catastrophic backtracking, and skipped rules or ran middleware for every path when the check flagged one. - Rewrites with
basePath: falseto internal destinations are rejected when the config loads, and rewrite destinations outside thebasePathresolve to nothing.
Like Next.js, Vinext now fails at config load and lists every invalid route: a source that path-to-regexp rejects, a source that compiles to a regex over 4,096 characters, or a malformed has/missing entry. An app with an invalid route that used to be skipped will now fail to start or build until it is fixed.
Worker Previews
@ktKongTong added previews deploy to @vinext/cloudflare, which builds your app and uploads it as a Worker Preview:
npx @vinext/cloudflare previews deploy pr-123
It needs a cloudflare.config.ts project and the cf CLI. The Preview name and the Vite mode are independent: pass --mode to change the build mode, and use isPreview in cloudflare.config.ts for Preview-only settings. Previews skip cache warming and version promotion. --skip-build uploads Preview output you built separately.
Cloudflare
- @JamesbbBriz fixed shared-cache renders that redirect. Since 1.0 they returned
500 TypeError: Too many redirectsinstead of the redirect, which broke Payload CMS's/adminlogin redirect. revalidatePath(),revalidateTag()andupdateTag()calls in one Server Action or Route Handler now send one CDN purge per request, as Next.js does, instead of one each. Workers Cache purges share a budget of 5 requests a minute per account, so an autosaving action could use it up within a minute and get a 500 even though its write had committed. A failed purge is now logged instead of failing the request, andrevalidatePath()skips the purge for paths onlyforce-dynamicroutes serve. @quantizor reported this.- @ash1day found that a request cancelled during app initialization could hang every later request in the isolate until it was evicted. On Workers, a pending promise belongs to the request that created it, and Vinext shares some across requests. The request that starts shared work, including a server
next/dynamicload, now stays alive until that work settles.
Server Actions and Route Handlers
- A fetch Server Action's arguments can only contain server references the current route owns, as in Next.js. A crafted request body could previously pass in an action owned by another route, such as one middleware protects. Progressive bound references go through the same check.
- @pitahashi fixed multipart POSTs to Route Handlers. They went through Server Action handling first, so bodies over 1 MiB got a 413 and cross-origin requests a 403. Progressive actions are now decided after late routing, against the final route.
- A route handler's
request.signalno longer stops aborting after a garbage collection. SSE handlers that clean up on abort were affected. - Route handler cookies and auth challenges are ordered after middleware's, as in Next.js.
res.appendHeaderworks in Pages API routes.
Other changes
- On
vinext start, middleware compared absoluteNextResponse.rewrite()targets against the client'sHostheader. A client could send aHostthat made an external rewrite target count as internal, so a page middleware protects rendered without its checks. Middleware now runs against the server's own origin, as in Next.js. - The Nitro
node-serverandbunpresets run middleware before servingpublic/files, as Next.js does, and serve public files the router resolves. @AhmedElBanna80 reported this, and that_buildManifest.jsalways had an emptysortedPages, which is also fixed. - After a client navigation, Back on iOS Safari opened the previous App Router page at the top. Vinext now writes the history entry before the new page replaces the old one, as Next.js does. @NathanDrake2406 reported this.
- @winstally reported that a page-slot redirect could keep refetching after a failed RSC fetch in WebKit. Hard navigations now suspend the tree, so redirect boundaries can't loop.
- @caumeira fixed
next/root-paramsreturningundefinedwhile a stale App Router page regenerated. - @owjs3901 fixed static export builds that could leave out prerendered pages, and still exit 0, when a keep-alive connection was reset.
- @Tiscs made
vinext startexit with an error when the port is in use. It used to log the error and exit 0. - @Arthur031221 made Pages Router dev reloads faster in large apps by keeping client asset dependency lists between loads.
- Pages that import CSS and set
dynamic = "error"no longer hang the dev server, as @Macil reported.dynamic = "error"pages that readsearchParamsno longer hang the RSC stream, and"use cache"pages error onsearchParamsas Next.js does. @heroui/reactworks in Server Components and is optimized by default, as @AndreasGalster2 reported.getImagePropsfromnext/imageworks in Server Components, as @GauravKumawat2002 reported.- The dev overlay ignores window error events without an error, such as the benign
ResizeObserver loop completedmessage @sensei-woo reported. - Sanitized server errors use React 19.3's
#441message, as Next.js does with React 19.3. vinext initupgrades a declared@vitejs/plugin-reactbelow Vinext's supported range, and Vinext skips unsupported copies the app resolves.@vercel/ogis imported again after a failed import,ipaddr.jsis bundled into production server builds for the image optimizer, and the Pages Routervinext startserver doesn't servepublic/files outside thebasePath.
Thanks to everyone who contributed to 1.3: @Arthur031221, @caumeira, @JamesbbBriz, @ktKongTong, @owjs3901, @pitahashi, @tamujin and @Tiscs, and to @AhmedElBanna80, @AndreasGalster2, @ash1day, @DaleyKD, @dppeak, @GauravKumawat2002, @jgeurts, @Macil, @NathanDrake2406, @quantizor, @sensei-woo, @williamrogersdev and @winstally for their bug reports.
The full lists of changes are in the release notes for vinext 1.3.0, @vinext/cloudflare 1.2.0 and @cloudflare/workers-response-store 1.1.2.