Skip to main content

Command Palette

Search for a command to run...

Bundlers Like Webpack and Vite: What Developers Miss

Published
6 min read
Bundlers Like Webpack and Vite: What Developers Miss

Bundlers don't "combine" your files. They map dependencies and output optimized chunks.

Most developers think bundlers work like this: "I run webpack, it reads all my files, merges them together, and outputs one big bundle."

It doesn't work that way. Bundlers don't merge. They analyze relationships, split intelligently, and create multiple output files designed for modern browsers and caching strategies.

1. The Core Idea: The Module Graph

At the heart of webpack and Vite is a module graph—a complete map of every file and how it depends on others.

When you tell webpack to build, it doesn't immediately process files. Instead:

  • It starts at your entry point (the main file you specify).

  • It parses that file and finds every import and require statement.

  • It recursively follows each dependency to build a complete graph.

  • Each file becomes a node. Each import relationship becomes an edge.

Webpack's official documentation calls this "recursively building a dependency graph that includes every module your application needs."

Why?
Because everything else—splitting, tree-shaking, optimization—depends on understanding this map first.

The module graph is the source of truth. Everything else is just processing this graph intelligently.

2. The Build Isn't Magic—It's Four Distinct Steps

Developers imagine bundlers work in one mysterious phase: "running webpack."

Actually, bundlers work in four clear, isolated stages:

Parse: Read and understand your JavaScript. Webpack parses the syntax and extracts import/require statements.

Resolve: Convert partial paths like ./utils or lodash into actual file locations on disk. Check if files exist. This is why webpack can find node_modules packages.

Transform: Use loaders and plugins to convert non-JavaScript assets. TypeScript becomes JavaScript. SCSS becomes CSS. JSX becomes regular functions. Each file gets individually transformed.

Bundle: Take the transformed module graph and generate output files (called "chunks"). Decide which modules go in which chunk based on your configuration.

These stages are sequential but independent. Each stage receives input and produces output that feeds the next stage. This is why bundlers are so extendable—plugins and loaders hook into specific stages and modify behavior.

3. Bundlers Never Execute Your Code—They Only Analyze It

This is crucial and misunderstood: Bundlers do not run your code.

When webpack processes const result = expensiveCalculation(), it doesn't calculate anything. It just sees:

  • There's a variable named result.

  • It's assigned the return value of a function call.

Bundlers use static analysis—understanding code structure without running it. This is possible because webpack looks at the static structure of ES2015 import and export statements.

Tree-shaking (removing unused code) works because bundlers can statically analyze which exports are used and which aren't, without executing anything.

Dynamic things break this:

  • require(someVariable) is opaque—bundlers can't know what file path the variable contains, so they conservatively include possibilities.

  • This is why bundlers warn about dynamic requires and why developers avoid them in production code.

Static analysis makes bundlers predictable, fast, and capable of optimization. But it also explains their limitations.

4. Output: Multiple Chunks, Not One File

Here's the misconception: "Webpack outputs a bundle"—singular.

Actually, webpack outputs one or more chunks. The relationship between your code and output can be 1:1, 1:many, or many:1.

By default, webpack creates:

  • One main chunk containing your application code.

  • Separate chunks for code you dynamically import using import().

  • Vendor chunks (if configured) containing node_modules code.

Why multiple chunks?
Because code splitting allows the browser to:

  • Load only the code needed for the current page.

  • Reuse cached chunks across pages.

  • Parallelize downloads.

Each chunk gets a unique name with a hash, like main.a3b4c5d6.js. This hash is content-based—if the chunk changes, webpack generates a new filename.

The browser never needs to refetch main.a3b4c5d6.js because the filename itself guarantees its content never changes. You can set Cache-Control: max-age=31536000 (one year), and the browser will cache it forever. If code changes, webpack generates main.e7f8g9h0.js, and the browser fetches the new file.

This chunking strategy is why webpack can scale to massive applications—unchanged code stays cached, and only changed chunks need redownloading.

5. Vite's Speed Secret: Native ESM + On-Demand Transformation

Webpack builds everything upfront. In large projects, this can mean waiting 5-30 seconds even during development.

Vite is different because it exploits native ES modules in browsers.

Here's how:

When you run vite dev:

  • Vite does not build your entire project.

  • Instead, it starts a dev server that serves files as ES modules.

  • When your browser requests a file, Vite transforms only that file and returns it.

  • Because browsers support native <script type="module">, the browser understands ES module syntax directly.

For dependencies in node_modules (which are often CommonJS), Vite uses esbuild to pre-bundle them once into ESM format. This pre-bundling happens only on first startup or when dependencies change, not on every reload.

When you save a file:

  • Vite detects the change.

  • It transforms only the changed file and its immediate dependents.

  • It sends an update via WebSocket to the browser.

  • The browser replaces the old module without a full reload—Hot Module Replacement (HMR).

This on-demand transformation is why Vite feels "instant" during development. No full rebuild on every keystroke.

However, for production, Vite uses Rollup (not esbuild). Rollup is optimized for generating efficient, optimized bundles with advanced code-splitting strategies—things that matter in production but are unnecessary during development.

6. Loaders Transform, Plugins Extend

Both webpack and Vite rely on an ecosystem of loaders and plugins to handle everything outside vanilla JavaScript.

Loaders transform individual files:

  • babel-loader converts ES6+ to ES5.

  • ts-loader converts TypeScript to JavaScript.

  • sass-loader converts SCSS to CSS.

  • Loaders work at the file level—each file is independently transformed.

Plugins operate at the bundle and compilation level:

  • They hook into webpack's lifecycle events (like compilation, optimize, emit).

  • They can modify the final output, generate new files, or inspect the entire module graph.

  • A plugin might minify code, extract CSS, generate an HTML file, or analyze bundle size.

This plugin system is why bundlers have lasted so long despite JavaScript ecosystem churn. New frameworks (React, Vue, Svelte) simply write loaders/plugins, and bundlers automatically support them.

7. Why Bundlers Scale So Insanely Well

Bundlers handle modern applications because:

  • Module graphs are deterministic: Given the same source code, the graph is always the same. No surprises.

  • Parsing is fast: Modern parsers like esbuild can parse thousands of files per second.

  • Static analysis enables optimization: Without running code, bundlers can eliminate dead code, split intelligently, and cache efficiently.

  • Caching strategies are built-in: Hash-based filenames + browser caching + chunking = only changed code is re-downloaded.

  • On-demand transformation (Vite): Don't transform what the browser won't request.

  • Parallel processing: Modern bundlers parallelize parsing, transforming, and bundling across CPU cores.

The result?

Webpack can process projects with tens of thousands of files. Vite can handle the same projects with sub-second dev server feedback.

Conclusion: Bundlers Are Deterministic Build Engines

Bundlers aren't magic. They're consistent, powerful processors that:

  • Map dependencies into a module graph.

  • Transform code through predictable stages.

  • Analyze statically, never executing your code.

  • Generate optimized, cacheable output chunks.

Understanding these fundamentals—the graph, the stages, the static analysis, the chunking strategy—means you can:

  • Debug configuration issues.

  • Optimize bundle size.

  • Choose the right tool for your project.

  • Extend bundlers for custom needs.

The reason bundlers work at scale is simple: they're not doing anything fancier than deterministic graph mapping + clever caching + parallel processing. But that simplicity, combined with extensibility, makes them powerful enough to handle the entire modern JavaScript ecosystem.