usability.cat
Issue Wiki

Large JavaScript Bundle

Packing your entire wardrobe for a weekend trip? That's what an oversized JS bundle does to your page load time.

What is this?

You are going away for the weekend. Do you pack your entire wardrobe — winter coats, formal wear, hiking boots, beach gear — or just what you need for two days? A large JavaScript bundle is the wardrobe-stuffing approach to web development. Your page sends megabytes of code to the browser, most of which is not needed for what the visitor is currently looking at. The browser has to download, parse, compile, and execute all of it before your page becomes interactive.

Why it matters

  • For your visitors: JavaScript is the most expensive resource on the web byte-for-byte. Unlike images (which display progressively), JS blocks interactivity. A 1MB bundle on a 3G connection takes over 10 seconds just to download — and then the browser still needs to parse and execute it. Your visitors tap buttons and nothing happens. They scroll and the page stutters.
  • For your business: Time to Interactive (TTI) directly correlates with conversions. Amazon found that every 100ms of additional load time cost them 1% of sales. Your hosting bill also climbs when you are serving megabytes of JavaScript on every page load.
  • The standard: Aim to keep your initial JavaScript bundle under 200KB compressed. Total JS on the page should ideally stay under 500KB compressed. Anything over 1MB compressed is a serious performance problem.
Code-split and tree-shaken
Route: /dashboard
├── framework.js   (45 KB gzipped)
├── dashboard.js   (22 KB gzipped)
└── chart-lib.js   (38 KB gzipped) ← loaded only on this route
Total: 105 KB
One massive bundle
Route: /dashboard
└── bundle.js (850 KB gzipped)
  ├── framework code
  ├── every page's code
  ├── 3 chart libraries (only 1 used)
  ├── moment.js (for 1 date format call)
  └── lodash (for 2 functions)
Total: 850 KB

How to fix it

React / Next.js

Next.js automatically code-splits by route, but you can make it even better with dynamic imports for heavy components.

import dynamic from "next/dynamic";

// Heavy chart library loads only when this component renders
const Chart = dynamic(() => import("@/components/chart"), {
  loading: () => <div className="h-64 animate-pulse bg-neutral-800" />,
});

// Markdown editor loads only when user clicks "Edit"
const Editor = dynamic(() => import("@/components/markdown-editor"), {
  ssr: false, // skip server rendering for client-only components
});

export default function Dashboard() {
  return (
    <main>
      <h1>Dashboard</h1>
      <Chart data={salesData} />
    </main>
  );
}

Replace heavyweight libraries with lighter alternatives:

// Instead of moment.js (67KB gzipped), use date-fns (tree-shakeable)
import { format } from "date-fns";
format(new Date(), "MMM d, yyyy"); // only imports what you use

// Instead of lodash (71KB), import individual functions
import debounce from "lodash/debounce"; // ~1KB instead of 71KB

// Or use native JS — you often don't need a library at all
const unique = [...new Set(array)]; // no lodash needed
const sorted = [...array].sort((a, b) => a.name.localeCompare(b.name));

Analyze your bundle to find what is taking up space:

# Next.js built-in analyzer
ANALYZE=true npm run build

# Or use source-map-explorer
npx source-map-explorer .next/static/chunks/*.js

Plain HTML

Split your JavaScript into smaller files and load only what each page needs.

<!-- Only load what the current page needs -->
<script src="/js/core.js" defer></script>

<!-- Conditionally load heavy features -->
<script>
  // Only load the chart library on pages that have charts
  if (document.querySelector("[data-chart]")) {
    const s = document.createElement("script");
    s.src = "/js/charts.js";
    s.defer = true;
    document.head.appendChild(s);
  }
</script>
High impactperformance~2 paws

The cat travels light and expects your JavaScript to do the same. Ship what you need, nothing more.

How the cat scores this

The scanner measures the total size of JavaScript loaded on the page, both compressed and uncompressed. It checks for common bloat patterns: duplicate libraries, full library imports where tree-shaking should apply (like importing all of lodash for one function), and JavaScript that could be dynamically imported instead. Bundles over 200KB compressed get a warning; over 500KB gets a significant penalty.

Further reading

On this page