Web Development

Webpack, Vite, Bundlers

2020. Evan Wallace publishes esbuild - a bundler written in Go. First benchmark: esbuild bundles three.js in 0.37 seconds. Webpack 4: 54.5 seconds. Difference: 100x. At that moment it became clear: JavaScript tooling written in JavaScript is the bottleneck. The era of Go and Rust tools began: esbuild, SWC, Turbopack.

  • **Vercel Next.js**: Turbopack (Rust) replaces Webpack in development - 700x faster HMR in large projects
  • **Vite** (6M weekly downloads): the standard for new React/Vue/Svelte projects, used in Nuxt 3, SvelteKit, Astro
  • **Shopify Hydrogen**: Vite + React Server Components for e-commerce - code splitting by default for each product

Bundling: From Thousands of Files to a Few

2010. A React application with 847 JS files. Without a bundler: 847 HTTP requests on load. With HTTP/1.1 - 6 parallel connections, sequential waterfall. Load time: 8-12 seconds. With webpack: 1-3 files, parallel load, 1.2 seconds. A bundler is not optional - it's a required tool.

A bundler builds a dependency graph: starting from the entry point (index.ts), it recursively resolves all import/require statements, creates a dependency graph, and merges into bundles. Webpack (2012) - the first mainstream bundler, configuration-heavy, flexible. Vite (2020) - development server on native ES modules + Rollup for production build. esbuild (2020) - written in Go, 10-100x faster than Webpack.

esbuild was written in Go by Evan Wallace (ex-Figma). Parallel parsing using Go goroutines vs JavaScript single-thread. Shared AST between passes vs separate ASTs. Result: TypeScript compilation 10x faster than tsc, bundling 100x faster than webpack. Vite uses esbuild for pre-bundling node_modules (CommonJS -> ES modules) and for TS/JSX transformation in dev mode.

0

1

Sign In

Vite uses native ES modules in development. Why not use them in production too?

Tree-Shaking: Eliminating Dead Code

Lodash in CommonJS: `import _ from 'lodash'` - 70KB minified. Only `_.debounce` is used. Without tree-shaking: all 70KB in the bundle. With tree-shaking via `import { debounce } from 'lodash-es'`: 2KB. A 35x difference - directly impacting LCP and First Contentful Paint.

Tree-shaking only works with ES modules. The reason: CommonJS (require) is dynamic, resolved at runtime. It's impossible to statically determine what's being imported. ES modules (import/export) are static, resolved at compile time. The bundler builds a usage graph: which exports are used, which aren't. Unused ones are 'shaken from the tree' and removed. Condition: pure functions without side effects (marked as `/*#__PURE__*/`).

Why many libraries publish both 'lodash' (CJS) and 'lodash-es' (ESM): CommonJS for server-side Node.js compatibility, ES modules for browser tree-shaking. In package.json: the `exports` field with `import` (ESM) and `require` (CJS) conditions. The bundler automatically picks the right variant. This is the dual package hazard: if one dependency imports the CJS version and another the ESM version - you may end up with two instances in the bundle.

A library exports 100 functions. The app uses 3. All 100 ended up in the bundle. What's wrong?

HMR: Hot Module Replacement Without Page Reload

A developer changes a button color in CSS. Without HMR: save -> full page reload -> lose state (form, scroll position, open modal) -> navigate back. Time: 5-10 seconds. With HMR: save -> only the changed module is replaced -> state preserved. Time: 50-200ms. For a developer, this is the difference between flow and constant interruption.

Webpack HMR: when a file changes, the bundler traverses the dependency graph, generates a hot update patch, the client applies it through the webpack HMR runtime. Problem: you need to manually add hot.accept() in modules or use framework-specific plugins (React Refresh, Vue HMR). Vite HMR: thanks to native ES modules, only one file changes without traversing the full graph - significantly faster.

React Fast Refresh is the official replacement for react-hot-loader by Facebook (2019). Key difference: Fast Refresh preserves React state when functional components change. Exception: if a component file exports not only a React component (e.g., constants or utility functions), Fast Refresh does a full remount. Hence the recommendation: one component = one file for optimal HMR.

HMR updated a React component but the state was reset. Why?

Code Splitting: Load Only What's Needed

An e-commerce homepage: the user needs the homepage, not the cart, checkout, or admin panel. Without code splitting: all app JS in one bundle - the user downloads 2MB to see the homepage. With code splitting: only 300KB for the homepage, the rest lazy-loaded on demand.

Route-based splitting: each route is a separate chunk, loaded on navigation. Component-based splitting: heavy components (rich text editor, PDF viewer, chart library) are loaded only when needed. Webpack: dynamic import() creates a split point. Vite: same API. React: React.lazy() + Suspense for code splitting at the component level.

Granular chunks vs fewer larger chunks: too fine-grained splitting creates many HTTP requests (even if parallel over HTTP/2). Too coarse - users download unnecessary code. Optimal chunk size: 50-250KB gzipped (Core Web Vitals recommendations). Webpack SplitChunksPlugin automatically merges frequently used modules into a shared chunk. Key anti-pattern: a 2MB vendor chunk - better to split into react, router, ui-library separately for better caching.

Vite is always faster than Webpack - all projects should migrate to Vite

Vite is significantly faster in development; in production, Webpack can give better results for complex configurations with advanced plugins

Vite's production build uses Rollup which falls behind Webpack in some advanced cases: Module Federation, custom output formats, complex chunk strategies. Webpack 5 Module Federation allows sharing bundles between micro-frontends - Vite doesn't support this natively. The choice depends on project requirements

After adding code splitting, the initial bundle size grew from 800KB to 1.2MB. What's the problem?

Related Topics

Bundlers sit at the intersection of JavaScript modules, performance, and DevOps:

  • JavaScript Modules — ES modules vs CommonJS - the fundamental difference affecting tree-shaking
  • Web Performance — Code splitting and tree-shaking directly affect LCP, FID, CLS
  • CI/CD Pipeline — Production build step in the pipeline: bundling, minification, optimization

Key Ideas

  • **Bundling**: dependency graph from entry point -> a few optimized files; Vite 100x faster than Webpack in dev via native ES modules
  • **Tree-shaking**: requires ES modules only; CommonJS = include everything; sideEffects in package.json controls what can be removed
  • **HMR**: Vite 50ms vs Webpack 2+ sec; React Fast Refresh preserves state; mixed exports = full remount
  • **Code splitting**: route-based + component-based lazy; shared vendor chunk for react/lodash; prefetch on hover for UX

Вопросы для размышления

  • Why was esbuild written in Go rather than JavaScript? What architectural decisions give it the 100x advantage?
  • Micro-frontend architecture: multiple teams deploy separate parts of one SPA. How do bundlers (Module Federation) help avoid duplicating react between teams?
  • Tree-shaking removes 'dead' code. What happens to code that has side effects on import? Give a real example of when this matters.

Связанные уроки

  • web-03 — JavaScript modules (ES modules, CommonJS) are the foundational concept that bundlers process
  • web-15 — WebSocket client code goes through the bundler before a production deploy
  • se-10 — CI/CD pipeline runs the build step through the bundler for each deploy
  • web-08 — Performance optimization: the bundler creates the foundation for Core Web Vitals (LCP, FID, CLS)
  • alg-11 — Dependency graph analysis in bundlers uses graph traversal algorithms
  • alg-18-topological
Webpack, Vite, Bundlers