Home/Learn/JavaScript & TypeScript/Bundlers — What They Actually Do

Bundlers — What They Actually Do

Intermediate
Tooling

A bundler resolves your import graph, transforms each file, and emits chunks the browser can load efficiently. Tree shaking and code splitting are the two outputs that matter for users.

Overview

Browsers can load ES modules natively, so it is fair to ask why a bundler exists at all. The answer is everything around the modules: hundreds of separate requests are slow, node_modules resolution is not a browser concept, TypeScript and JSX need transforming, and unused code should not ship. A bundler does all of that and emits a small number of hashed files. You rarely configure one from scratch — Next and Vite come pre-configured — but understanding tree shaking and code splitting is what lets you diagnose a 2MB bundle.

Tree Shaking

Dropping code nothing imports. It only works under specific conditions, which is why it often silently does not.

Why your bundle still contains lodash
// This ships one function, not the whole library
import { debounce } from 'lodash-es'

// This ships all of lodash — CommonJS cannot be statically analysed
const _ = require('lodash')
import _ from 'lodash'

// Requirements for tree shaking to work:
//   1. ES modules (static import/export), not CommonJS
//   2. No side effects at module top level
//   3. "sideEffects": false in the package's package.json

// A top-level side effect defeats it:
// utils.js
export function unused() { }
window.analytics = init()      // runs on import, so the module is kept

// Barrel files are the usual culprit in application code:
// components/index.ts re-exporting 80 components means importing
// one can pull the graph of all 80 into the analysis.
import { Button } from '@/components'        // risky
import { Button } from '@/components/Button' // predictable

Code Splitting

Not everything is needed on first paint. A dynamic import becomes a separate chunk fetched on demand.

Dynamic import as a split point
// Static — always in the initial bundle
import { Editor } from './Editor'

// Dynamic — its own chunk, fetched when this line runs
const { Editor } = await import('./Editor')

// In React
const Editor = lazy(() => import('./Editor'))
<Suspense fallback={<Skeleton />}><Editor /></Suspense>

// In Next
const Editor = dynamic(() => import('./Editor'), {
  ssr: false,               // for anything that touches window
  loading: () => <Skeleton />,
})

// What is worth splitting: the code editor, chart libraries,
// markdown renderers, PDF generators, admin-only routes, modals.
// Anything heavy that most visitors never open.

// Routes split automatically in Next — each page is its own chunk.

Diagnosing Size

Measure before optimising. The answer is almost always one oversized dependency.

Analyse, then cut the biggest thing
# Next
ANALYZE=true npm run build          # with @next/bundle-analyzer

# Any project
npx source-map-explorer 'dist/**/*.js'
npx vite-bundle-visualizer

// The usual offenders and their fixes:
//   moment           -> date-fns, or Intl
//   lodash           -> lodash-es, or a native method
//   an icon set      -> import individual icons, never the barrel
//   a chart library  -> dynamic import
//   a duplicate dep  -> two versions of react in the tree; npm ls react

// Budgets, so a regression is caught in review rather than in a
// support ticket: fail CI if the initial chunk exceeds a threshold.

Key Points to Remember

  • 1A bundler resolves the import graph, transforms files and emits a few hashed chunks instead of hundreds of requests
  • 2Tree shaking requires ES modules, no top-level side effects, and a sideEffects declaration — CommonJS defeats it
  • 3Barrel files can pull far more into the graph than the one component you imported
  • 4A dynamic import creates a separate chunk loaded on demand — split editors, charts, modals and admin routes
  • 5Measure with a bundle analyzer before optimising; the cause is usually one oversized dependency

Interview Questions

Sign in to ask Aria
1

What is tree shaking and what stops it from working?

Medium
2

How does a dynamic import differ from a static one at build time?

Medium
3

Your initial JavaScript bundle is 2MB. How do you find out why?

Medium

Ask Aria about Bundlers — What They Actually Do

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…