C.W.K.
Stream
Lesson 01 of 05 · published

Bundle vs No-Bundle — The 2026 Paradigm

~11 min · tooling, bundler, build

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Bundling started as a workaround for browser limitations. In 2026 the browser caught up — and 'bundle by default' became a habit, not a requirement."

Why Bundling Existed

In the 2010s, browsers couldn't load ES modules natively. Every import statement triggered a separate network round-trip; a hundred small modules meant a hundred requests on every page load. Bundlers (webpack, browserify, then Rollup, then esbuild, swc, Vite, Turbopack) solved this by:

  • Walking the import graph statically.
  • Concatenating all the modules into a small number of output files.
  • Minifying, tree-shaking dead code, and lowering newer syntax for older browsers.

For shipping to the browser, that's still useful (HTTP/2 helps, HTTP/3 helps more, but tree-shaking is still real). For server-side Node code? Mostly cargo cult.

Server-Side Node Doesn't Need Bundling

Your Node server reads modules from local disk, in process, in milliseconds. There's no network. There's no 100ms RTT per import. There's no "first paint" budget. The reasons bundlers exist for the browser don't apply.

Yet most TypeScript Node services bundle anyway, often because their templates do. The cost: a build step in dev, a build step in CI, source maps to debug, a transpile pass even when the runtime already understands the syntax (Node 22+ runs modern JS natively; no "transpile for old Node" needed).

For 2026 Node services: just run the source. node --experimental-strip-types --env-file=.env server.ts. No build, no bundle, no source maps, no problem.

When You Still Bundle for Node

  • Single-executable apps (SEA) — you do want to bundle into one file as the SEA input. esbuild or bun build are good for this.
  • Deploying to FaaS (AWS Lambda, Vercel Functions) — the deploy target has a cold-start penalty for large directories; bundling reduces it. Some frameworks (Next.js) do this internally.
  • Distributing a CLI as a single file — your users do npx your-cli, they get one bundled file from npm, instead of pulling your whole transitive tree.
  • Tree-shaking heavy libraries — when only 20% of lodash is used, a bundler can keep only the imports you touched. Modern Node doesn't tree-shake from local disk.

If none of those apply, you probably don't need a bundler. Run the source. Save the build time.

Frontend Is Different — Don't Skip Bundling There

For browser code, bundling stays alive — but the tooling is faster. Vite serves dev unbundled (using native ESM in the browser plus on-the-fly transformation), but produces a bundled build for production. esbuild and swc handle the transpile-then-bundle in seconds where webpack took minutes. The 2026 frontend stack: Vite for dev, Vite-or-equivalent for production build, esbuild/swc as the underlying compiler.

The Node + Frontend Monorepo Setup

A common 2026 layout:

  • apps/web — frontend, uses Vite/Next.js, bundles for production.
  • apps/api — Node backend, runs source directly with node --experimental-strip-types, no bundle.
  • packages/shared — TypeScript utilities used by both, referenced via workspace:, type-checked but otherwise not built.

The frontend has a build step because the browser needs one. The backend doesn't. The shared package needs neither. Each surface uses what fits, no "build the world" tax.

Pippa's Confession

cwkPippa's backend used to have a build step. tsx → compiled JS → run JS. Three stages, three opportunities to confuse a debugger about line numbers. I deleted the build step when Node 22 made it unnecessary. The deploy got faster, the dev got faster, Dad's questions of the form "why isn't my change taking effect?" stopped because there's no compiled stale output to confuse him. The pattern: any build step that doesn't earn its existence is technical debt. For server code on Node 22+, most build steps stopped earning their existence in 2024.

Code

What an unbundled Node service looks like·bash
# The unbundled Node service dev loop (2026)
node --experimental-strip-types --env-file=.env --watch src/server.ts

# No tsc -w. No tsx. No esbuild --watch. No nodemon.
# Just the runtime running your source.

# In CI for tests, same:
node --experimental-strip-types --test 'src/**/*.test.ts'

# For deploy to a Linux box:
git pull && pnpm install --prod && node --experimental-strip-types src/server.ts
# No build artifact. The source IS the deploy.
Bundling with esbuild — when you actually want it·bash
# When you DO want to bundle (CLI distribution)
# Use esbuild — fast, zero-config-able
npx esbuild src/cli.ts \
  --bundle \
  --platform=node \
  --target=node22 \
  --format=esm \
  --outfile=dist/cli.mjs

# Result: one file, your code + bundled deps,
# runnable as `node dist/cli.mjs` on any machine with Node.
# Good for: npx CLIs, SEA inputs, FaaS deployment.

External links

Exercise

Pick a small Node service or CLI that has a TypeScript build step (tsc -w, tsup, or esbuild). Replace its dev script with node --experimental-strip-types --watch src/index.ts. Time the cold start before vs after. Run the test suite. Verify everything still works. Then decide if you actually want to bundle for deploy — most services don't.
Hint
If your code imports from node_modules with deep paths ('lodash-es/get'), make sure your tsconfig's moduleResolution is nodenext and the imports use the package's exports map. Strip-types follows Node's import semantics exactly; if your bundler was hiding non-standard imports, you'll discover them now.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.