If you’ve run PageSpeed Insights on your Nuxt 4 app and seen the dreaded “Eliminate render-blocking resources” warning pointing at your CSS files, you’re not alone. This is a common issue with Nuxt 4’s SSR pipeline, and most solutions floating around the internet are subtly broken.
The Problem
Nuxt 4 SSR does something clever: it inlines your component styles directly into the HTML response as <style> tags. This means the browser can start rendering immediately without waiting for external files.
But here’s the catch — Nuxt also injects <link rel="stylesheet"> tags in the <head> pointing to the same CSS files. These external stylesheets are completely redundant, yet they’re render-blocking. The browser stops painting and waits for them to download first, adding 500-1500ms to your First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
The typical offenders:
entry.{hash}.css— your global CSS + Tailwind base stylesvirtual_public.{hash}.css— component-scoped styleslazy-hydrated-component.{hash}.css— island and lazy-loaded styles
The Common Fix (That Doesn’t Work)
Search for this problem and you’ll find dozens of blog posts recommending a build:manifest hook like this:
// nuxt.config.ts
hooks: {
'build:manifest': (manifest) => {
for (const key of Object.keys(manifest)) {
const entry = manifest[key];
if (entry.resourceType === 'style' || key.endsWith('.css')) {
entry.dynamicImports = [];
entry.css = [];
}
}
},
},
This looks reasonable. It finds CSS entries in the manifest and clears their references. But it doesn’t work.
The problem is subtle. This code only clears css arrays on entries that are CSS files. But JavaScript entries — like entry.js and component chunks — also have css: ["entry.{hash}.css"] in their manifest entries. Nuxt reads those JS entry css arrays when generating client.precomputed.mjs, which computes the styles, preload, and prefetch objects used during SSR to inject <link> tags.
In other words, the fix targets the wrong entries.
The Correct Fix
The solution is a one-line change: move entry.css = [] outside the if block so it runs on all manifest entries, including JavaScript chunks:
// nuxt.config.ts
hooks: {
'build:manifest': (manifest) => {
for (const key of Object.keys(manifest)) {
const entry = manifest[key];
if (entry.resourceType === 'style' || key.endsWith('.css')) {
entry.dynamicImports = [];
}
// Clear css references from ALL entries (including JS chunks)
// to prevent Nuxt from injecting render-blocking <link> tags.
// Styles are already inlined via SSR.
entry.css = [];
}
},
},
That’s it. The key difference: every entry in the manifest — whether it’s a CSS file or a JavaScript chunk — gets its css array cleared.
Why This Works
Here’s the data flow for CSS injection in Nuxt 4:
1. Vite generates a build manifest
2. build:manifest hook can modify it
3. Nuxt reads the manifest and generates client.precomputed.mjs
4. SSR renderer reads client.precomputed.mjs
5. Renderer injects <link rel="stylesheet"> for entries found in styles/preload/prefetch
The broken fix modifies step 2, but only for CSS file entries. Since JS entries still reference CSS files, step 3 picks them up anyway and the <link> tags end up back in your HTML.
The correct fix clears CSS references from JS entries too, so nothing makes it past step 3.
Importantly, this doesn’t break anything:
- SSR styles still work — they’re inlined as
<style>tags in the HTML body, completely independent of the manifest - Client-side navigation still works — the CSS files remain in the build output and get loaded on demand during SPA navigation
- The only thing removed is the redundant, render-blocking
<link rel="stylesheet">tags on initial page load
How to Verify
After applying the fix:
- Build your app:
npm run build - Check the precomputed file: Look at
.output/public/_nuxt/builds/latest.jsonorclient.precomputed.mjs— CSS arrays should all be empty[] - View page source: Start your server and inspect the HTML — there should be no
<link rel="stylesheet">tags in<head> - Run PageSpeed Insights: The “Eliminate render-blocking resources” warning for CSS should be gone
Results
In our case, applying this fix removed three render-blocking stylesheet requests and improved FCP by roughly 800ms on mobile connections. Your mileage will vary depending on CSS bundle size and server location, but removing unnecessary round trips before first paint is always a win.