Edge Bundle Optimization Techniques
Edge runtimes reject or penalize oversized bundles. Cloudflare Workers and Vercel Edge Middleware both enforce a 1 MB uncompressed limit; Netlify Edge Functions allow 20 MB. Compression applied during transit does not count toward these deployment thresholds—the uncompressed size of your bundled JavaScript is what matters.
Beyond hard rejection, bundle size directly correlates with initialization latency. Each V8 isolate must parse, compile, and JIT-compile every function in the bundle before the first request executes. A 900 KB bundle adds approximately 15–25 ms to cold start initialization on a typical edge isolate. Reducing the bundle to 200 KB reduces that overhead proportionally.
This guide covers the techniques and provider limits. For a step-by-step reduction walkthrough that drives a real artifact under 1 MB, follow optimizing bundle size for edge runtime deployment.
Tree-Shaking and ESM-First Resolution
Tree-shaking removes unused exports but requires ESM modules with static import/export declarations. CommonJS modules (require(), module.exports) cannot be statically analyzed and are included in their entirety.
Practical steps:
- Set
"type": "module"and"sideEffects": falsein yourpackage.json. - Use
import { fn } from 'library'notimport library from 'library'for partially-used packages. - Avoid barrel files (
index.tsthat re-exports everything) — they force bundlers to retain all exports.
// ESM static import (tree-shakeable)
import { verifyJWT } from '@edge/jwt';
// Conditional import for heavy, rarely-used operations
let cryptoLib: typeof import('crypto-js') | null = null;
export async function handler(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/health') {
return new Response('OK', { status: 200 });
}
// Load heavy dependency only when the specific route requires it
if (url.searchParams.has('legacy_hash')) {
cryptoLib ??= await import('crypto-js');
const hash = cryptoLib.SHA256(url.searchParams.get('legacy_hash')!).toString();
return new Response(JSON.stringify({ hash }), {
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
Provider Bundle Limits
| Provider | Uncompressed Limit | Key Constraints |
|---|---|---|
| Cloudflare Workers | 1 MB | ESM imports; nodejs_compat flag for Node shims |
| Vercel Edge Middleware | 1 MB | node_modules excluded from bundle; Edge Config for runtime data |
| Netlify Edge Functions | 20 MB | Deno import maps; explicit polyfill bundling |
Replacing Heavy Dependencies
These are the most common bundle-size offenders and their replacements:
| Heavy dependency | Replacement | Savings |
|---|---|---|
lodash (full) |
Individual functions or native equivalents | 70–500 KB |
moment |
Intl.DateTimeFormat, date-fns (tree-shaken) |
200–300 KB |
axios |
Native fetch |
10–50 KB |
node-fetch |
Native fetch |
10–50 KB |
uuid |
crypto.randomUUID() |
5–15 KB |
AWS SDK v2 |
@aws-sdk/client-* v3 (modular) |
200–800 KB |
jsonwebtoken |
jose (ESM, edge-compatible) |
50–200 KB |
Minification and Build Configuration
// esbuild.config.mjs
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/edge-handler.ts'],
bundle: true,
minify: true,
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
target: ['es2022'],
platform: 'browser',
define: {
'process.env.NODE_ENV': '"production"',
},
external: ['node:*'], // Exclude all node: prefixed modules; use nodejs_compat instead
outdir: 'dist/edge',
metafile: true,
});
The metafile: true option produces a JSON file that maps every module to its byte contribution. Feed it to the esbuild bundle analyzer:
npx esbuild-bundle-visualizer --metafile dist/meta.json
Vite Configuration for Edge Deployment
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'esnext',
rollupOptions: {
external: ['node:crypto', 'node:fs', 'node:path', 'node:http', 'node:net'],
output: {
inlineDynamicImports: false,
manualChunks: (id) => {
if (id.includes('node_modules')) return 'vendor';
},
},
},
},
});
CI Bundle Size Gate
Automate rejection of PRs that exceed the 1 MB limit:
# .github/workflows/edge-bundle-audit.yml
name: Edge Bundle Size Gate
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx wrangler deploy --dry-run --outdir=dist
- name: Validate Uncompressed Size
run: |
SIZE=$(wc -c < dist/worker.js)
LIMIT=1048576
if [ "$SIZE" -gt "$LIMIT" ]; then
echo "Bundle exceeds 1 MB uncompressed: $SIZE bytes"
exit 1
fi
echo "Bundle OK: $SIZE bytes"
For Next.js / Vercel builds, use the @next/bundle-analyzer package to generate a treemap and identify bloated imports:
ANALYZE=true next build
Debugging Transitive Dependencies
Indirect imports from third-party packages are the most common cause of unexpected bundle bloat. Identify them:
# Cloudflare: generate metafile during dry-run
wrangler deploy --dry-run --outdir=dist
npx esbuild dist/worker.js --bundle --analyze=verbose 2>&1 | head -60
# Vite: generate bundle stats
npx vite build --mode production && npx rollup-plugin-visualizer
Flag packages that import lodash, moment, crypto-browserify, or buffer as transitive dependencies. Replace the root package with an edge-compatible alternative.
Deployment Decision Flow
| Phase | Action | Gate |
|---|---|---|
| Audit | Run --analyze; map dependency weight; flag non-ESM packages |
If node_modules > 60% of payload, enforce sideEffects: false |
| Refactor | Replace heavy libraries; add dynamic import() for non-critical paths |
No process, fs, path without guards |
| Bundle | Configure minification; NODE_ENV=production stripping |
Minification should reduce payload by > 30% |
| Validate | CI size gate; local edge emulation for init latency | Uncompressed < provider limit |
| Deploy | Staging first; monitor cold-start metrics | TTFB < 100 ms on cold start; memory < 80 MB |
For step-by-step bundle reduction targeting sub-1 MB deployment artifacts, see optimizing bundle size for edge runtime deployment.
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Script too large on deploy |
Uncompressed bundle exceeds the 1 MB Cloudflare/Vercel cap | Replace the heaviest transitive dependency; gate size in CI |
| Tree-shaking removes nothing | Missing "sideEffects": false or a CJS package |
Declare sideEffects: false; switch to an ESM build of the dependency |
| Whole library pulled in for one helper | Default import or a barrel file | Use named imports; import the specific submodule path |
| Bundle balloons after adding a small util | Transitive lodash/moment dependency |
Trace with --analyze; swap the root package for an edge-compatible one |
| Cold start regresses without size change | A polyfill suite added init-time work | Scope polyfills; prefer native Web APIs |
Runtime-Constraints Checklist
-
package.jsondeclares"type": "module"and"sideEffects": false - Heavy dependencies (
lodash,moment,axios,jsonwebtoken -
node:*
Frequently Asked Questions
Does the 1 MB limit apply to the compressed or uncompressed bundle?
Uncompressed. Cloudflare Workers and Vercel Edge measure the raw bundled JavaScript, not the gzipped or Brotli transit payload. A bundle that compresses to 300 KB can still be rejected if its uncompressed size is over 1 MB.
Why is my bundle large even though I import only one function?
The package is likely CommonJS or lacks "sideEffects": false, so the bundler cannot statically prove the rest is unused and retains the whole module. A default import or a barrel file has the same effect. Use named imports from an ESM build.
What is the single highest-leverage optimization?
Replacing one large transitive dependency—lodash (full), moment, or AWS SDK v2—with a native Web API or a modular edge-compatible package. This routinely removes more weight than minification and tree-shaking combined.
Does Netlify's 20 MB limit mean bundle size does not matter there?
Bundle size still affects cold-start parse time even when the hard cap is generous. The Deno runtime must compile what you ship, so trimming weight reduces initialization latency regardless of the headroom.
How do I find transitive dependencies inflating the bundle?
Generate a metafile during a dry-run build and feed it to a bundle visualizer, or run esbuild --analyze=verbose. Flag any package that pulls in lodash, moment, crypto-browserify, or buffer, then replace the root package.
Related
- Optimizing bundle size for edge runtime deployment
- Polyfill strategies for Node.js APIs at the edge
- Supported Web APIs in edge runtimes
- Managing cold starts in serverless environments
- Memory and CPU limits across edge providers
Conclusion
Bundle optimization at the edge is non-optional when deploying to Cloudflare Workers or Vercel Edge Middleware. The 1 MB uncompressed limit is a hard rejection threshold, not a soft warning. Beyond compliance, every kilobyte removed directly reduces initialization latency. Start with dependency replacement (the highest-leverage action), enforce tree-shaking via ESM and "sideEffects": false, and gate bundle size in CI before it becomes a production incident.