The Problem
I still see the same Lighthouse warning on App Router projects that otherwise look clean: the hero image is optimized, the page is server rendered, priority is set correctly, and LCP is still late because one or two generated CSS chunk files are blocking the first paint.
That is not a fake problem. A recent Stack Overflow thread on the App Router shows developers hitting large generated CSS chunks on real homepages and asking the right question: are those files supposed to block, and how do you shrink them without breaking styling?
In practice, the answer is yes, some of them are expected to block. CSS that is needed for the initial route has to arrive before the browser can paint that route correctly. The real mistake is shipping too much CSS in that critical path.
If your homepage pulls in a giant globals.css, Swiper styles, Font Awesome styles, Bootstrap utilities, and a couple of theme files from the root layout, Next.js will happily include that in the initial route styles. Your LCP problem is not "Next.js CSS is broken." It is that the wrong CSS became critical.
For a broader performance workflow, my Core Web Vitals guide covers how I confirm the LCP candidate before I start moving code around.
Why It Happens
The key detail from the Next.js CSS docs is that production builds automatically chunk and merge CSS based on how and where you import it. Import order matters, and anything truly global stays global.
That creates three common failure modes:
app/layout.tsximports too much. Teams start with Tailwind or a small reset, then keep appending third-party styles there because it "works everywhere."- Interactive components drag large library CSS into the first route. Sliders, map libraries, icon packs, and editor styles are frequent offenders.
- Component CSS is technically scoped, but the component sits above the fold on the homepage, so every unnecessary style inside that subtree becomes part of the initial paint budget.
The result is predictable: the browser waits on the CSS, LCP moves right, and the hero image or headline gets blamed even though the stylesheet payload is the real blocker.
The Fix
I use the same sequence every time.
1. Keep only true globals in the root layout
Your root layout should contain the minimum set of styles the whole site genuinely needs:
// app/layout.tsx
import './globals.css'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
If you have a slider stylesheet, admin-only styles, dashboard utilities, or one-off marketing page CSS in that layout, move it out. The App Router will not save you from a bad import boundary.
2. Scope heavy styles to the component that actually needs them
When a homepage hero uses an interactive library, I split the static LCP content from the enhanced client feature:
// app/page.tsx
import HeroShell from '@/components/hero-shell'
import HeroCarousel from '@/components/hero-carousel'
export default function HomePage() {
return (
<>
<HeroShell />
<HeroCarousel />
</>
)
}
// components/hero-carousel.tsx
'use client'
import 'swiper/css'
import { Swiper, SwiperSlide } from 'swiper/react'
export default function HeroCarousel() {
return (
<Swiper>
<SwiperSlide>Slide 1</SwiperSlide>
<SwiperSlide>Slide 2</SwiperSlide>
</Swiper>
)
}
That does not make Swiper free, but it prevents me from polluting the root layout with library CSS that the rest of the site never uses.
3. Replace broad global styles with CSS Modules where possible
If a stylesheet exists only to style one section, give it a smaller boundary:
// components/hero-shell.tsx
import styles from './hero-shell.module.css'
export default function HeroShell() {
return (
<section className={styles.hero}>
<h1>Senior Full Stack Developer</h1>
<p>Performance-focused Next.js and WordPress builds.</p>
</section>
)
}
That makes the build graph easier to reason about. It also reduces the odds of a 3 KB component stylesheet hitching a ride inside a 90 KB global file because nobody knew where it belonged.
4. Audit third-party styles before you blame Next.js
If a chunk is big, the first thing I look for is not a framework bug. It is imported CSS from:
- Bootstrap or Tailwind add-ons
- slider libraries
- icon font packages
- WYSIWYG editors
- large theme files copied over from legacy builds
Those imports are usually the difference between an okay homepage and a slow one.
5. Verify on the production build, not just next dev
The docs call this out directly: CSS ordering and behavior can differ between development and production. I always inspect the real build output and re-run Lighthouse or WebPageTest against the deployed page before I declare the issue fixed.
If your App Router project is already tangled with caching and route-level invalidation work, my Next.js revalidateTag fix is worth reading too, because a lot of slow pages turn out to have both data and styling problems at the same time.
The Practical Rule
Render-blocking CSS is not something you "turn off" on a page that needs styling. What you can do is aggressively control what gets promoted into the critical route CSS.
That means:
- root layout for true globals only
- route or component imports for everything else
- CSS Modules instead of bloated shared files
- no lazy assumptions about third-party CSS cost
That is the fix I keep shipping on marketing sites where the homepage looks visually light but the CSS graph says otherwise.
If your homepage is still slow after image and font cleanup, I can usually isolate the blocking CSS path quickly. See my services if you want me to audit the critical rendering path on a real production build.
