Frontend Engineering/
Lesson

JavaScript is the most expensive asset on the web byte-for-byte. A 200KB image and a 200KB JavaScript file do not have the same cost. The image just needs to be decoded and painted. The JavaScript needs to be parsed, compiled, and executed, all on the main thread, all blocking interactivity. On a mid-range phone, 1MB of JavaScript can take 3-5 seconds just to parse.

This is why bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. optimization matters so much. Every kilobyte of JavaScript you ship has a direct, measurable impact on how fast your page becomes interactive.

Understanding your bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together.

Before optimizing, you need to see what is in your bundle. Vite and webpack both have visualization tools that show you exactly which libraries and files are contributing to the total size.

# Vite - generates a visual treemap of your bundle
npx vite build
npx vite-bundle-visualizer

# Webpack - similar visualization
npx webpack-bundle-analyzer dist/stats.json

The first time you run a bundle analyzerWhat is bundle analyzer?A tool that visualizes the size and composition of your JavaScript bundle to identify what to optimize., you will almost certainly find surprises. A library you imported once for a single function might be contributing 100KB. A barrel import (import { x } from './utils') might be pulling in 50 files when you need 1.

AI pitfall
AI-generated code is the number one source of bundle bloat. When you ask AI to "add a date picker," it imports a full date library. When you ask for "a debounce function," it imports all of lodash. Always run a bundle analysis after accepting AI-generated code that adds new dependencies.
02

The cost of JavaScript vs other assets

To understand why bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. optimization matters more than optimizing other asset types, consider how the browser processes each one:

Asset typeDownloadParseCompileExecuteBlocks main thread
ImageYesDecode (off-thread)NoNoNo
CSSYesParse CSSOMNoNoNo (after paint)
JavaScriptYesParse ASTJIT compileExecuteYes
FontYesDecode (off-thread)NoNoNo

JavaScript is the only asset that blocks the main thread during every processing stage. A 200KB JS file costs 5-10x more in time-to-interactive than a 200KB image.

03

Tree shakingWhat is tree shaking?A build tool feature that removes unused exported code from your final bundle, reducing the amount of JavaScript shipped to users.

Tree shaking is the process of removing unused code from your final bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together.. Modern bundlers (Vite, webpack, Rollup) do this automatically, but only when your code uses ES moduleWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. syntax.

// Tree-shakeable: bundler can see you only use 'debounce'
import { debounce } from './utils';

// NOT tree-shakeable: bundler can't analyze dynamic access
const utils = require('./utils');
const debounce = utils.debounce;
PatternTree-shakeable?Why
import { fn } from 'lib'YesStatic analysis can track the reference
import * as lib from 'lib'PartiallyOnly if you use specific properties
require('lib')NoCommonJS is dynamic, cannot be statically analyzed
import lib from 'lib' (default)DependsWorks if the library is properly structured

Barrel fileWhat is barrel file?An index.js that re-exports from multiple modules in one place so consumers can import everything from a single path - can hurt tree shaking if overused. problem

Barrel files (an index.ts that re-exports everything from a directory) can defeat tree shaking:

// utils/index.ts - barrel file
export { formatDate } from './date';
export { formatCurrency } from './currency';
export { parseCSV } from './csv';        // 50KB dependency
export { renderChart } from './chart';    // 200KB dependency

// Your code - you only need formatDate
import { formatDate } from './utils';
// But depending on the bundler, you might get ALL of the above

The fix is to import directly from the source file:

// Direct import - guaranteed to only include what you need
import { formatDate } from './utils/date';
AI pitfall
When AI generates utility files, it almost always creates a barrel file that re-exports everything. This is convenient for code organization but can be devastating for bundle size. When AI creates a utils/index.ts with 20 exports, importing one function can pull in all 20 and their dependencies. Always check if a barrel import is hiding unnecessary code in your bundle.
04

Replacing heavy libraries

One of the highest-leverage optimizations is replacing heavy libraries with lighter alternatives that do the same thing. AI tools default to the most popular library, which is often not the lightest.

Heavy librarySize (minified)Lighter alternativeSize (minified)Savings
moment.js67KBdate-fns (per function)2-5KB93%
lodash (full)70KBlodash-es (per function)1-3KB96%
chart.js200KBlightweight-charts45KB78%
axios13KBNative fetch0KB100%
uuid12KBcrypto.randomUUID()0KB100%
classnames1.5KBTemplate literal0KB100%
// What AI generates - 67KB for one date format
import moment from 'moment';
const formatted = moment(date).format('YYYY-MM-DD');

// What you should use - 2KB
import { format } from 'date-fns';
const formatted = format(date, 'yyyy-MM-dd');

// Even better - 0KB, built into the platform
const formatted = date.toISOString().split('T')[0];
AI pitfall
AI loves moment.js, axios, and full lodash imports because they dominate the training data. Every time AI adds one of these, check if a lighter alternative or a native API does the job. fetch() is built into every modern browser, you almost never need axios. crypto.randomUUID() replaces the entire uuid package. structuredClone() replaces lodash's cloneDeep.
05

Dynamic imports for heavy features

Not every feature needs to be in the initial bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together.. If a feature is only used by some users or only appears after an interaction, load it dynamically:

// Bad: PDF library (500KB) loaded for every user
import { PDFDocument } from 'pdf-lib';

// Good: loaded only when user clicks "Export PDF"
async function exportPDF(data) {
  const { PDFDocument } = await import('pdf-lib');
  const doc = await PDFDocument.create();
  // ... generate PDF
}

This pattern works for any heavy feature: rich text editors, chart libraries, image processing, CSV parsing, or anything that only a subset of users will trigger.

Decision table: when to dynamically import

ScenarioStatic importDynamic import
Used on every page loadYesNo
Used by <20% of usersNoYes
Behind a user interaction (click, toggle)NoYes
Below the foldNoYes
Core UI framework (React, routing)YesNo
Feature-specific heavy libraryNoYes
06

Analyzing import cost

Before adding any dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself., check its cost:

# Check bundle size impact of a package
npx bundlephobia <package-name>

# Or use the website: https://bundlephobia.com
What to checkWhy
Minified sizeRaw download cost
Gzipped sizeActual transfer cost
Tree-shakeable?Can you import just what you need?
DependenciesDoes it pull in other heavy packages?
Native alternative?Does the browser already do this?
07

Code splittingWhat is code splitting?Breaking your application into smaller JavaScript chunks that load on demand so users only download the code needed for the page they're viewing. strategies

There are three main strategies for splitting your bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together.:

Route-based splitting

Each page gets its own chunk. This is the highest-impact, lowest-effort strategy:

// Each route is a separate chunk - users only download
// the code for the page they are visiting
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

Component-based splitting

Heavy components that are not visible on initial load get their own chunks:

// Modal content, charts, editors - anything below the fold
// or behind a user action
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const ChartDashboard = lazy(() => import('./ChartDashboard'));

Library-based splitting

Configure your bundler to put large third-party libraries in separate chunks:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'chart': ['chart.js'],
        }
      }
    }
  }
});
08

Practical workflow

Here is the workflow for optimizing a bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together.:

  1. Measure: Run bundle analyzerWhat is bundle analyzer?A tool that visualizes the size and composition of your JavaScript bundle to identify what to optimize. to see current state
  2. Identify: Find the largest contributors
  3. Replace: Swap heavy libraries for lighter alternatives or native APIs
  4. Split: Move heavy features behind dynamic imports
  5. Verify: Run bundle analyzer again to confirm improvement
  6. Budget: Set a bundle size limit in your CI to prevent regression
javascript
// Bundle optimization patterns

// 1. Replace heavy libraries with native APIs
// Instead of axios (13KB):
const response = await fetch('/api/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
});
const result = await response.json();

// Instead of uuid (12KB):
const id = crypto.randomUUID();

// Instead of lodash.cloneDeep (15KB):
const clone = structuredClone(original);

// 2. Dynamic import pattern
async function handleExport(format) {
  if (format === 'pdf') {
    const { generatePDF } = await import('./exporters/pdf');
    await generatePDF(data);
  } else if (format === 'csv') {
    const { generateCSV } = await import('./exporters/csv');
    generateCSV(data);
  }
}

// 3. Vite manual chunks configuration
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'react-vendor';
            if (id.includes('chart')) return 'chart-vendor';
            return 'vendor';
          }
        }
      }
    }
  }
});