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.

Edge bundle optimization pipeline A bundle passes through audit, dependency replacement, tree-shaking, minification, and a CI size gate before deployment. Audit --analyze Replace deps lodash, moment Tree-shake ESM, sideEffects Minify esbuild CI gate < 1 MB Each stage shrinks the uncompressed artifact and the cold-start parse cost
Bundle optimization is a pipeline: audit weight, replace heavy dependencies, tree-shake, minify, and gate the final size in CI.

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": false in your package.json.
  • Use import { fn } from 'library' not import library from 'library' for partially-used packages.
  • Avoid barrel files (index.ts that 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.json declares "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.

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.