September 8, 2025
TL;DR
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.
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 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:
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"
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
}
}
}
Download our service guide to understand how we can help you optimise your site speed
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}"]
}
}
}
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}</>
}
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.
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.
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.
Download our service guide to understand how
we can help you optimise your site speed