Inlining critical CSS is recommended to minimize the time to First Contentful Paint. See for example this Wearediagram article.
Some bundlers can automatically extract critical CSS from your stylesheets. To my knowledge, Esbuild does not offer this feature, and I have been unable to find any plugins that do.
I thought that inlining critical CSS might be part of Tailwind’s rendering optimizations. However, aside from pruning unused utility classes, I couldn’t find a feature specifically for inlining critical CSS.
Has anyone found a hassle-free method for automatically inlining critical CSS?
P.S. There are some tools that could help, like Critical and Penthouse. I haven’t used those yet. And I was actually a bit surprised to see that Tailwind and Esbuild haven’t targeted this feature yet. But maybe with good reason?
When on JS land I used to do this, but since the resurgence of utility first CSS libraries like Tailwind I haven’t seen this tackled.I haven’t read on it, but I suppose the nature of atomic CSS reduces the importance of critical stylesheets?
Indeed, that thought crossed my mind. There might not typically be a significant difference between the utility classes used above the fold compared to those used throughout the rest of the page.
Certain UI components may only be present above the fold, which makes it beneficial to include their CSS classes and IDs in a critical stylesheet when using regular CSS. However, with utility classes, it’s more ‘random’—for lack of a better term—regarding which classes are used for components above or below the fold.
This seems like a limitation inherent to a utility-first approach.
I don’t know of any tools, but I’m curious if your CSS files are truly the biggest bottleneck for reaching First Contentful Paint? And if so, on what percentage of page views is this true?
Regardless of your answers to the above, this article from Harry Roberts breaks down the difficulty of reliably implementing and maintaining inlined-critical-css, as well as why it might not give you the expected bang/buck.
Given that CSS files are generally cached (and compressed!), your users are probably only experiencing CSS fetch bottlenecks on their first visit, and those first visits can be optimized by leveraging a CDN for asset paths, and implementing HTTP 103 Early Hints (via plug_early_hints | Hex) to preemptively fetch your CSS while the origin server is doing any sort of additional IO/work to build out the actual HTML response. Cheers!
I stand by the recommendation above, but one method for inlining css so that it’s only present if the component is rendered is to use inline <style></style> tags within your HEEX components, and to do so above the elements in the component so that the styles are available by the time the markup is parsed. You’d then have to not fetch the standard stylesheet on the entrypoint pages.
That said, I wouldn’t use this approach for oft-recurring components like <.link> or <.button> due to the repetition it would introduce to the markup, but it could be useful for if you have componentry that’s only present on entry/landing pages (due to my note above about cacheability of CSS files).
Thanks for sharing the links. It was useful to research your suggestions.
And about whether render blocking CSS is the problem or not. I’m not sure actually at this point. The app is still too far from what it’s going to be at launch. And, to the point from the article you referenced, it might be difficult to predict even then!
From my experience, on higher latency connections and lower-end devices, using a CDN for assets actually made things slower. A lot of this was due to DNS, establishing a new TCP connection, etc.
I don’t recall the numbers off-hand, but it was between 10%-30% improvement for my site / layout. Anecdotally, I had a 5 year old phone and was out in the hinterlands, I noticed the difference immediately. Reducing the number of origins made an incredible difference, not in raw speed, but in reliability. Nothing is more frustrating than a load being fast most of the time and then getting a half-load 1/20 times.
Also, regarding CDN performance, one of the advantages used to be that common js libraries (jquery) that were pulled from the CDN likely did not have to be downloaded as they would already be cached in the browser. I believe a security change was made a few years ago that prevented the re-use of cache, so that advantage is now gone.
Ah, I should clarify. When I say I recommend a CDN for assets, I don’t mean the old school shared CDN, as those are no longer useful due to the sandboxing security change you mentioned above. I was referring to hosting your static assets via a CDN service like AWS Cloudfront or Azure Front Door rather than relying on your origin server to serve them, but I should have made that clearer.
Out of the box, using an asset CDN at a different domain or subdomain would introduce another DNS lookup, but that one additional lookup should be minimal and hopefully unfelt if you leverage HTTP 103 for preemptive preconnects/preloads, which, thankfully, are getting broader and broader browser support.
Understood. I don’t think that ends up making much of a difference as an extra lookup and an extra tcp connection is there anyway.
I need to understand the effect of HTTP 103 a little better, but it’s useful to divide the user experience into several buckets.
Frustrated. Users want consistency, even if it’s slow consistently. Frustration is caused by tail latencies. I’ve experienced it myself and seen the “Connecting to x.y.z” hang for 10-15 seconds and having the site half-render. Or “Looking up x.y.z…” Probably will never hear from these users because they’ll just quit using your site. So I made everything come from a single domain to avoid this problem.
Acceptable. Most sites are slow, janky, filled with ads, so the bar is pretty low. You don’t have to make any effort.
Happy. The site has to be fast and user-friendly. Performance is a feature. Optimizing for user tasks is a feature. The combination is excessively rare, so combine the two and you’ll get have genuinely happy people thank you.
The transfer sizes:
compressed page is 60kB.
compressed css is 10kB
compressed js is 90kB.
fonts are 166kB
The first load for critical path requires 70kB. Another 90kB for interactivity. And finally the fonts adjust the cumulative layout shift slightly which don’t matter.
Since I’m trying to avoid frustrated users, from my experience, the overhead of DNS and SSL connection establishment over low-performance connections matters more than the raw size.