Describe the bug
I intend to submit a PR for this issue if the team agrees on the direction.
adapter-vercel uses two completely different bundling strategies depending on the runtime:
- Edge functions → bundled with esbuild (
bundle: true, tree-shaking, single file output)
- Serverless functions → traced with
@vercel/nft only (no bundling, no tree-shaking, entire files are copied)
This means serverless functions ship significantly more code than necessary. @vercel/nft operates at the file level — if your code imports one function from a module, nft includes the entire file (and all of its file-level dependencies). Dead code within files is never eliminated.
This directly undermines the purpose of the split option. The whole point of split: true is to produce lean, per-route functions with fast cold starts. But since nft can't tree-shake, every split function still drags in the full dependency graph of every file it touches. You're paying the cost of splitting (more functions, more cold starts) without getting the benefit (smaller functions).
Where this happens in the code
In packages/adapter-vercel/index.js, the create_function_bundle function handles serverless:
async function create_function_bundle(builder, entry, dir, config) {
// ...
const traced = await nodeFileTrace([entry], { base });
// ... copies traced files as-is, no bundling step
}
Meanwhile, generate_edge_function in the same file already does full esbuild bundling:
const result = await esbuild.build({
entryPoints: [`${tmp}/edge.js`],
outfile: `${outdir}/index.js`,
bundle: true,
platform: 'browser',
format: 'esm',
// ...
});
The tooling is already there — esbuild is already a dependency of adapter-vercel. It's just not used for the serverless path. And the edge runtime is being deprecated, so the only path forward is serverless — which has no tree-shaking.
Impact
- Bloated function sizes: serverless functions include dead code that esbuild would eliminate
- Slower cold starts: more code to load = longer initialization, which is the primary cost in serverless
split doesn't deliver on its promise: per-route functions are still bloated because nft can't remove unused exports within files
- Size limit issues: users are more likely to hit the 250MB function size limit because nft includes far more code than necessary
How other frameworks handle this
No framework on Vercel does true tree-shaking for serverless — they all use @vercel/nft. But Next.js mitigates the problem with optimizePackageImports, which rewrites barrel file imports at compile time before nft traces them. Vercel reports up to 40% faster cold starts from this alone.
SvelteKit's adapter-vercel has no equivalent mitigation. The server entry goes straight to nft with no import optimization, no bundling, nothing.
Historical context
The adapter used to bundle serverless functions with esbuild but this was later replaced with nft — likely because esbuild bundling was breaking edge cases with native addons, dynamic requires, or CJS/ESM interop.
That was a reasonable decision at the time, but the tradeoff is becoming harder to justify as apps grow and cold starts become a bigger concern. At minimum, an opt-in bundling option would let users who don't rely on native addons get the performance they need.
Proposal
Use esbuild to bundle the serverless function entry point (similar to what's already done for edge functions), with platform: 'node' instead of platform: 'browser'. This would produce a tree-shaken bundle before the function is packaged.
// Instead of just nft tracing:
const traced = await nodeFileTrace([entry], { base });
// Bundle first, then package:
await esbuild.build({
entryPoints: [entry],
outfile: `${dir}/index.js`,
bundle: true,
platform: 'node',
format: 'esm',
// handle native addons / binaries as external
});
Considerations:
- Native addons / binary dependencies (e.g.,
sharp, prisma): would need to be marked as external, similar to the existing external option for edge functions.
- Side-effect-dependent modules: esbuild's
sideEffects handling should cover most cases, but worth testing.
- Opt-in vs default: could start as an opt-in option (e.g.,
adapter({ bundleServerless: true })) before becoming the default.
- Why not ncc? esbuild is already a dependency and already used for edge in this adapter — no new deps, consistent tooling, faster than webpack.
Reproduction
This is a structural issue in the adapter, not a runtime bug. No specific app is needed to reproduce — it affects every serverless deployment.
To observe:
- Build any SvelteKit app with
adapter-vercel
- Inspect
.vercel/output/functions/<name>.func/ — you'll see full unshaken files from node_modules copied in by nft
- Compare with what
npx esbuild <entry> --bundle --platform=node --format=esm produces for the same entry point
The difference in output size is the dead code that nft ships but esbuild would eliminate.
Logs
No runtime error — this is a build output optimization issue.
System Info
Node: 22.19.0
Yarn: 4.13.0
npm: 11.12.0
Severity
serious, but I can work around it
Additional Information
No response
Describe the bug
I intend to submit a PR for this issue if the team agrees on the direction.
adapter-verceluses two completely different bundling strategies depending on the runtime:bundle: true, tree-shaking, single file output)@vercel/nftonly (no bundling, no tree-shaking, entire files are copied)This means serverless functions ship significantly more code than necessary.
@vercel/nftoperates at the file level — if your code imports one function from a module, nft includes the entire file (and all of its file-level dependencies). Dead code within files is never eliminated.This directly undermines the purpose of the
splitoption. The whole point ofsplit: trueis to produce lean, per-route functions with fast cold starts. But since nft can't tree-shake, every split function still drags in the full dependency graph of every file it touches. You're paying the cost of splitting (more functions, more cold starts) without getting the benefit (smaller functions).Where this happens in the code
In
packages/adapter-vercel/index.js, thecreate_function_bundlefunction handles serverless:Meanwhile,
generate_edge_functionin the same file already does full esbuild bundling:The tooling is already there — esbuild is already a dependency of adapter-vercel. It's just not used for the serverless path. And the edge runtime is being deprecated, so the only path forward is serverless — which has no tree-shaking.
Impact
splitdoesn't deliver on its promise: per-route functions are still bloated because nft can't remove unused exports within filesHow other frameworks handle this
No framework on Vercel does true tree-shaking for serverless — they all use
@vercel/nft. But Next.js mitigates the problem withoptimizePackageImports, which rewrites barrel file imports at compile time before nft traces them. Vercel reports up to 40% faster cold starts from this alone.SvelteKit's adapter-vercel has no equivalent mitigation. The server entry goes straight to nft with no import optimization, no bundling, nothing.
Historical context
The adapter used to bundle serverless functions with esbuild but this was later replaced with nft — likely because esbuild bundling was breaking edge cases with native addons, dynamic requires, or CJS/ESM interop.
That was a reasonable decision at the time, but the tradeoff is becoming harder to justify as apps grow and cold starts become a bigger concern. At minimum, an opt-in bundling option would let users who don't rely on native addons get the performance they need.
Proposal
Use esbuild to bundle the serverless function entry point (similar to what's already done for edge functions), with
platform: 'node'instead ofplatform: 'browser'. This would produce a tree-shaken bundle before the function is packaged.Considerations:
sharp,prisma): would need to be marked asexternal, similar to the existingexternaloption for edge functions.sideEffectshandling should cover most cases, but worth testing.adapter({ bundleServerless: true })) before becoming the default.Reproduction
This is a structural issue in the adapter, not a runtime bug. No specific app is needed to reproduce — it affects every serverless deployment.
To observe:
adapter-vercel.vercel/output/functions/<name>.func/— you'll see full unshaken files fromnode_modulescopied in by nftnpx esbuild <entry> --bundle --platform=node --format=esmproduces for the same entry pointThe difference in output size is the dead code that nft ships but esbuild would eliminate.
Logs
System Info
Severity
serious, but I can work around it
Additional Information
No response