Faster LiveView renders with inlined critical CSS

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?

1 Like

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.

2 Likes

Also, I am currently not using Tailwind yet. I am still on the fence about whether to use it or not.

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!

2 Likes

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).

This is something I’ve never worried about before. There is some context missing from this article, but I’m thinking this is more of an optimization you’d take on if 99.9% of your DOM is being created by JavaScript after parsing a bunch of JSON. Perhaps even more so if you are using one of those CSS-in-JS-style thingies that create hashed class names (I’m actually not sure how efficient those are). It seems to be implied that’s the paradigm the author—and 95% of the web dev community—works in. LiveView is already getting a massive initial render boost by having raw HTML sent to the browser, so I wouldn’t worry about this… I don’t think? I’ve only worked in a Modern JS Framework™ once for a couple of years but other than that I’ve always worked server-side sending HTML and never had any trouble keep initial renders super snappy. At least not resorting to this kind of thing. The culprit would always be more tangible stuff like a slow DB query or someone forgetting to shrink an image.

2 Likes

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.

Definitely need to try this.

And just in case people don’t know about it, https://www.webpagetest.org/ is a great resource.

2 Likes

I am using css-scope-inline. It uses Mutation Observer and re runs on dom change.

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.