Add support for critical CSS inlining with App Router #59989
Replies: 20 comments 83 replies
-
There is no way to inline critical CSS on App router. I can't find any solution to do it now. I don't know why Nextjs not support for App router @mlstubblefield do you have any solution to solve it now? |
Beta Was this translation helpful? Give feedback.
-
Yea, it's a bummer- we saw huuuge improvements in our FCP when we first added critters, so I'm sad to see it no longer function. |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
Critters does not support streaming. This experimental flag works in the Pages Router (no streaming) but does not work with the App Router (has streaming). I've updated this issue to be a feature request and converted it to a discussion. |
Beta Was this translation helpful? Give feedback.
-
Our results in the field were quite significant. Here are our field results using optimizeCss. |
Beta Was this translation helpful? Give feedback.
-
@leerob any update on this request? |
Beta Was this translation helpful? Give feedback.
-
I'm testing a postbuild script to get inlined critical css with critters on html files for SSG pages, for the moment just in preview branches and on the homepage only. import Critters from "critters";
import fs from "fs";
const critters = new Critters({
path: ".next/static/css",
publicPath: "/_next/static/css/",
inlineFonts: true,
preloadFonts: false, // next is already preloading them
});
async function editHome() {
const inlined = await critters.process(
fs.readFileSync(".next/server/app/index.html", "utf8")
);
fs.writeFileSync(".next/server/app/index.html", inlined);
}
new Promise(() => editHome()); |
Beta Was this translation helpful? Give feedback.
-
tldr: Under Chrome "Slow 3G" conditions, it appears that inlining critical css makes the difference between good and mediocre/poor Core Web Vitals outcomes using the app router. I have performed some lab benchmarks with a small GKE-deployed NextJS 14.1.0 ssr application with some suspended async operations (waterfall screenshot below). The waterfall suggests that inlining critical CSS would have a substantial positive performance impact on FCP in high-latency and/or low-bandwidth situations. Below is a screenshot of the waterfall for the site, with disabled cache and Chrome "Slow 3G" (2 seconds request latency, 400 kbps down). Because Chrome adds two seconds of latency to each request, the fully-loaded HTML document is available two seconds before the critical (external) CSS finishes loading. (In low-bandwith conditions, even with fetch priority set properly, the external CSS is competing with the loading Javascript, making it load even more slowly. If the CSS were inlined, the high latency and low bandwidth means the inline CSS/html has several seconds at 400 kbps down where it is the only resource being fetched, so high latency might actually help FCP in low-bandwidth situations!) From a "qualitative" perspective, the slow FCP in this situation makes a lot of the (really cool) NextJS preloading UI optimizations less impressive, because the suspended SSR (especially with non-dynamic routes) finishes long before critical CSS is even ready! Without inlining critical CSS under these network conditions, preloading only looks good if the browser has a warm cache. With my use case, I expect greater-than-normal visits under bad network conditions and no browser cache. The most important site content is text, and I want to display that immediately. (NextJS caching is a fantastic tool for this!) Aside from the inline CSS issue, the NextJS app router is performing beautifully. (In fact, CWV for mobile/desktop are generally well above 90%.) However, I feel bad that in spite of the technical backflips I'm doing, anyone inlining their critical CSS is going to massively outperform my app on FCP in bad network conditions. I am also particularly sensitive to performance under bad conditions because I am hoping to use benchmarks as marketing materials (both personally as a developer and for the product), and it's tough to give a talk on web performance when your critical CSS is not inline. In some ways, I feel like the sword character from Indiana Jones, doing weeks of next-gen optimizations and being taken out by style={{backgroundColor: 'black'}}. If I am misreading or misinterpreting that waterfall screenshot, please let me know. As always, thank you maintainers! |
Beta Was this translation helpful? Give feedback.
-
This is very important to get it working with the App Router! |
Beta Was this translation helpful? Give feedback.
-
It's so frustrating, guys. At this point im pretty much regret all this time spent. could live with "not ideal" ssr stuff in page router Sorry for the rant, but it was really painful and hard to pull off and now i guess i will have to revert refactoring because of this css stuff |
Beta Was this translation helpful? Give feedback.
-
Hi. Is there an update about adding support for Critical CSS? I Spent more than a week migrating my app to app-roter only to realize this feature was missing. It was a lapse on my part not to notice this before migration 😞 Considering how significant this can be for the page-load experience boost, I can't help but wait to have the support added to take my effort and app to production If anyone is already working on this please do mention it here... I'd be happy to help in any way possible. |
Beta Was this translation helpful? Give feedback.
-
Similar result from https://danielnagy.me/posts/Post_tsr8q6sx37pl. Inlining critical CSS matters on mobile according to this benchmark: https://youtu.be/1gZmkpsVGkk?si=q8pNnWqkJLYzqjlt Sebastian made a similar point: |
Beta Was this translation helpful? Give feedback.
-
HI. Since there are problems dealing with critical css with dynamic routes, why aren't you just providing a possibility to use a global css embedded in the head? Many projects only use a global css with tailwind and this solution already solves many performance problems and user experience. This is frustrating, an entire application being limited to already known performance problems. |
Beta Was this translation helpful? Give feedback.
-
Hi everyone, Just want to give an update that our team has recently discussed critters/this discussion further! As mentioned above, this is unfortunately currently not compatible with App Router because of streaming. We're currently investigating how this would compare with Partial Prerendering (PPR) via a method like link-header prefetching, which could be an alternative way of achieving the same performance benefits as critters. We will report back on this thread with new updates! |
Beta Was this translation helpful? Give feedback.
-
Method for SSG static pages onlyI've worked a few hours on my previous script and I got it working. No css is loaded on first-page landing (when the html file is effectively used), so no blocking request (actually no css request at all) nor additional css kbs. In my case I had no significant improvement on performance tests, so I haven't fully tested it, as I'm probably not gonna use it in production. Anyway I got a perfect layout on all the pages I tested, within a quite complex tailwind based app. Hope it might be helpful for someone! Implementation
"postbuild": "tsx src/postbuild.ts",
/* eslint-disable no-useless-escape */
import { load } from 'cheerio';
import Critters from 'critters';
import fs from 'fs';
import path from 'path';
const critters = new Critters({
path: '.next/static/css',
publicPath: '/_next/static/css/',
inlineFonts: true,
preloadFonts: false,
preload: 'body',
logLevel: 'error',
});
function fromDir(startPath: string, filter: string) {
if (!fs.existsSync(startPath)) {
console.log('No .next folder found. Aborting.');
} else {
const files = fs.readdirSync(startPath);
// iterates over files and folders
Promise.all(
files.map(async file => {
const filename = path.join(startPath, file);
const stat = fs.lstatSync(filename);
// recursion in case stat is directory
if (stat?.isDirectory()) {
fromDir(filename, filter);
// checks if the file is an html
} else if (filename.endsWith(filter)) {
// inlines css and moves <link rel=stylesheet>s to body
const inlined = await critters.process(
fs.readFileSync(filename, 'utf8')
);
//writes critters' changes
fs.writeFileSync(filename, inlined);
//loads page on cheerio (html editor)
const page = load(fs.readFileSync(filename, 'utf8'));
page('script')?.each((i, elem) => {
const text = page(elem).text();
// If a <script> tag (RSC payload) contains a css import, removes it
if (text?.match(/\[[^\]^\[]+_next\/static\/css\/.*?\.css.*?\]/)) {
const newText = text.replaceAll(
/\[[^\]^\[]+_next\/static\/css\/.*?\.css.*?\]/g,
'[]'
);
page(elem).text(newText);
}
});
//removes <link rel=stylesheet>s to avoid css loading.
// Remove this part in case you might need non-blocking css load
page('[rel="stylesheet"]')?.each((i, elem) => {
page(elem).remove();
});
// writes cheerio's changes
fs.writeFileSync(filename, page.html());
}
})
);
}
}
const promise = new Promise(() => fromDir('.next/server/app', '.html'));
promise.then(() => console.log('Postbuild done'));
|
Beta Was this translation helpful? Give feedback.
-
@samcx Do we have any ETA on this? This issue is having a lot of impact on our LCP and impacting our SEO performance. |
Beta Was this translation helpful? Give feedback.
-
Terribly disappointed. Not a fan of React, even less of a fan of React-based server frameworks, only confirms negative perceptions about this part of frontend-land. It's an important feature for a server framework - please add support! |
Beta Was this translation helpful? Give feedback.
-
@gujral1997 v15 has necessarily landed yet—it's still a release candidate. It's going to land with React v19 when that's released on latest.
There is work being done for this (although not directly mentioned in the PR) → #67715. We will continue to share any important updates as they come! |
Beta Was this translation helpful? Give feedback.
-
Was this fixed in NextJs version 15? |
Beta Was this translation helpful? Give feedback.
-
Why does Lighthouse report that I only have this problem in the mobile version? |
Beta Was this translation helpful? Give feedback.
-
To Reproduce
const nextConfig = { experimental:{ optimizeCss: true } }
Critical CSS is blocking :( and not optimized!
Current vs. Expected behavior
Before the appDir, critical css was inlined and FCP would be really really fast.
Now, you can see that FCP is behind the blocking CSS!
Just look at this waterfall https://www.webpagetest.org/result/231027_BiDcZF_FDJ/1/details/#waterfall_view_step1
Beta Was this translation helpful? Give feedback.
All reactions