adapter-vercel: serverless functions lack tree-shaking (esbuild is already used for edge, but not for serverless)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 20.8k
- Forks
- 2.3k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 156
Description
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/nftonly (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
splitdoesn'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 asexternal, similar to the existingexternaloption for edge functions. - Side-effect-dependent modules: esbuild's
sideEffectshandling 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 fromnode_modulescopied in by nft - Compare with what
npx esbuild <entry> --bundle --platform=node --format=esmproduces 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/adapter-vercel/index.js by reading create_function_bundle and comparing it with generate_edge_function. Build an adapter-vercel app and inspect .vercel/output/functions/.func/ alongside an esbuild bundle to understand the current size difference. Done means the serverless path has an agreed bundling approach that preserves native and binary dependencies while removing unused code.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- build-system, cloud
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100