We help companies like yours with expert Next.js performance advice and auditing.

nextjs, bundle size
08/09/2025 13:17

How a Major E‑Commerce Site Ended Up with a 24 MB Next.js Bundle - and How to Avoid CSS Bloat

Next.js CSS bloat explained: 24 MB bundles, 240k classes, real fixes

media

TL;DR

  • Root cause: a giant CSS Modules class‑mapping object (240,000 entries) imported at global app scope, injecting megabytes of string literals into the _app chunk on every route.
  • Size snapshot (rounded): 24 MB minified / 4.1 MB compressed for the homepage; _app 19 MB minified / 3.8 MB compressed.
  • Practical fixes: don’t module‑import utility CSS, enable strict purge/JIT, route‑scope CSS, and add CI budgets + a simple class‑map detector.

Large JavaScript bundles are a common performance killer in modern web apps, but sometimes the root cause isn’t JavaScript logic at all-it’s CSS. Heavy CSS/JS also hurts interactivity - see our deep‑dive on INP and long frames: Enhancing Web Performance with the Long Animation Frames API. In this post, we’ll unpack a real-world case from a major e‑commerce site where a single Next.js chunk ballooned to 19 MB minified / 3.8 MB compressed, largely due to CSS class name bloat. We’ll cover what happened, why it happened, and-most importantly-how you can optimize CSS in Next.js to avoid the same fate.

The case: 240,000 CSS classes inflated a bundle

Our team analyzed a large Next.js e‑commerce application that was shipping an unusually large client bundle - 24 MB minified / 4.1 MB compressed on a typical homepage. That’s far above the industry norms we see across tens of thousands of Next.js sites. (For broader context, see our Next.js bundle sizes study).

Digging deeper, we found that a single chunk - the Next.js _app bundle - was responsible for most of this bloat, weighing in at 19 MB minified / 3.8 MB compressed. Inside that bundle, we discovered a massive object mapping original CSS class names to module-scoped, hashed equivalents- a telltale sign of a CSS Modules (or similar) build exporting class names as an object. Here’s a small sample (simplified for readability):

{
  "u-push-7/12@wide": "site_u-push-7__12__wide__riKbU",
  "u-push-8/12@wide": "site_u-push-8__12__wide__QAT6C",
  "u-push-9/12@wide": "site_u-push-9__12__wide__2B_nM",
  // ...hundreds of thousands more entries...
}

Each entry maps an original CSS class (like "u-push-7/12@wide" ) to a generated class (like "site_u-push-7__12__wide__riKbU"). Multiply that by 240,000 and you’re staring at megabytes of string literals shipped to the client for no runtime purpose-the app merely references these styles by key, and the mapping object just sits in memory.

Observed lab edit (one‑off): Manually stripping unused mapping keys reduced the compressed JS by ~75% (3.8 MB => ~0.95 MB) and the minified size also by ~75% (19 MB => 4.75 MB) on that build. This was a destructive local edit for sizing the problem - not a shippable fix.

How did so much CSS end up in the JS bundle?

How do 240,000 style classes sneak into a Next.js app? In this case, it came down to how the CSS was integrated into the application:

  • Global Utility CSS imported at the App-level: The site applied a large internal utility framework by importing it at the pages/_app.* level. Because it was imported as a module (not a plain global stylesheet), the build exported a massive JS object mapping every class. Importing this high up meant every page paid the cost.
  • Lack of purging/tree-shaking for CSS: Unlike JavaScript, unused CSS isn’t removed by default. If you feed tens of thousands of utilities into the build without a precise purge step or JIT mode, you’ll ship everything.
  • Overly granular, variant-heavy utilities: The framework generated classes for countless fractional widths, breakpoints, and state variants (e.g., u-push-7/12@wide, u-push-8/12@wide, etc.). Most pages needed only a subset, but the build shipped the entire universe.
  • CSS Modules for a global utility sheet: CSS Modules are great for component-scoped styles, but they’re a poor fit for giant utility libraries; the exported mapping turns the stylesheet into JS bloat. If you’re diagnosing bloat, start by visualizing bundles with Source Map Explorer.

How to fix it (copy‑pasteable playbook)

1) Do not import utility frameworks as CSS Modules

Turning a huge stylesheet into a module exports a giant mapping object. Import it as global CSS instead-or better, use a JIT utility approach so you only ship what you use.

// BIG mapping object in JS
- import styles from "../styles/utilities.module.css"
// Global CSS, no JS map
+ import "../styles/utilities.css"

2) Enable strict purge/JIT (pick one)

Tailwind JIT (preferred) - keep globs tight, remove broad safelists, and trim scales/screens to what you actually use.

// tailwind.config.js
module.exports = {
  content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
  safelist: [],
  theme: {
    screens: { sm: "640px", md: "768px", lg: "1024px" },
    extend: {
      // keep scales tight; avoid dozens of fractional widths
    }
  }
}

Get our monthly web performance news roundup!

No spam - everything you need to know about in web performance with exclusive insights from our team.

Targeted safelist examples for dynamic class names:

// tailwind.config.js (inside module.exports)
safelist: [
  { pattern: /^btn-(primary|secondary|ghost)$/ },
  { pattern: /^col-span-(1|2|3|4|5|6|7|8|9|10|11|12)$/ }
]

Legacy purge - if you’re on a custom utility stack, use a purge step (e.g., PurgeCSS) with a disciplined extractor and a minimal safelist.

// postcss.config.js
module.exports = {
  plugins: {
    "@fullhuman/postcss-purgecss": {
      content: ["./app/**/*.{tsx,ts,js,jsx,html}", "./components/**/*.{tsx,ts}"]
    }
  }
}

3) Route‑scope CSS in the App Router

Only load heavy styles where they’re needed. (Related Next.js deep‑dive: identifying memory leaks in Next.js.)

// app/(product)/layout.tsx
import "./product.css" // Only for the product subtree
export default function ProductLayout({ children }: { children: React.ReactNode }) {
  return <>{children}</>
}

4) Add bundle budgets in CI (prevent regressions)

Fail builds when the main payload creeps above your budget. For a walkthrough with examples, see Automate Bundle Budgets in CI/CD in our guide: Optimizing Next.js Performance: Bundles, Lazy Loading, and Images.

5) Reduce combinatorial variants

Avoid vast grids of fractional classes (e.g., 7/12). Prefer CSS Grid with semantic spans, and use clamp() plus container queries to replace multiple @wide/breakpoint variants.

Limitations & edge cases

  • Dynamic class names (computed strings) can defeat purge/JIT. Where necessary, restrict patterns and add targeted safelist rules rather than blanket safelists.
  • Keep broad imports out of the top‑level layout, route‑scope them instead.
  • Third‑party component libraries may ship large, unused CSS. Audit coverage and, where possible, opt into on‑demand or CSS‑in‑JS variants that tree‑shake better.

Conclusion

CSS is a double‑edged sword in Next.js performance. It’s easy to ship a mountain of unused classes-especially when utility frameworks or CSS Modules are misapplied globally. As noted in the one‑off lab edit above, removing unused mapping keys suggested a potential ~75% reduction of the bundle size. The shippable fixes here focus on preventing such bloat at the source while preserving the site’s look and feel.

By applying the strategies above - purging unused styles, scoping imports by route, avoiding module-mapped utility sheets, and leveraging route-level CSS with the App Router - you’ll dramatically reduce bundle sizes so users only load what’s necessary for the page they’re on.

At the end of the day, performance is about attention to detail. Watch for accidental complexity-like shipping an entire design system’s class map to the client-and keep CSS lean, scoped, and intentional.

Get Your Free Consultation Today

Don’t let a slow Next.js app continue to impact your commercial goals. Get a free consultation
with one of our performance engineers and discover how you can achieve faster load times,
happier customers, and a significant increase in your key commercial KPIs.