DEV Community

loading...
Cover image for 🎨 Optimising CSS Delivery

🎨 Optimising CSS Delivery

thekashey profile image Anton Korzunov ・6 min read

What would you say, if I told you - delivering JS is a super simple task while delivering CSS is way, WAY MORE COMPLEX? And I've just said it!

CSS

How many ways to define styles you know? I could name 3:

  • inline styles. Basic and most inefficient way.
  • inlined <style>s. The HTML way to define CSS. Better, although not cacheable.
  • external .css files. The HTTP way to define to deliver styles for the page, and the only one cache-friendly one.

CSS-in-JS

But the first two options do not require a separate .css file to work - they could be delivered to the page using .js as transport.


This is what CSS-in-JS is - css, defined and delivered in js.


Even the third option does not require .css file - the link to the blob url(which is URL) could be generated from JavaScript. So - what does makes CSS-in-JS so special, and so delightfull? A Critical Style Extraction!

Let me be completely honest - CSS-in-JS usually sucks from the "speed" point of view - JS has a significant cost, and delivering the same styles via plain CSS would be much more efficient and should be preferred at all times if you are looking for the best user experience.

However keep in mind - some modern css-in-js solutions( linaria, astroturf, treat) are becoming pure CSS after the build step.

However, there are two moments, where "real" CSS-in-JS just shines:

  • complex auto-generated styles(like dark mode). As long you can autogenerate them in-place, not create 100500 different styles using SASS loop (or your own hands). Sometimes, rarely but still, css-in-js might be much much more compact than a pure css.
  • SSR. Where you can inline styles next to their usage and, actually, creation (in terms if StyledComponents). As a result, you will be able to render a page before fetching real style files(that would take a while). And even more - before parsing all styles on the page(better say - downloading), as long as styles will be interleaved with HTML in a most efficient way.

It's like - defining styles for header just before header, as well as defining styles footer just before footer - so you don't have to load anything "extra" to display the next block of HTML. That's makes (HTML) rendering process more streamlined and efficient on slow connections. I mean FAR more efficient, and that's matters! First load matter!

First load

And we need the same goodness for a "normal" CSS

Optimizing CSS delivery

CSS is a very nasty thing - while we are loading our scripts as async or defer - CSS is always sync. And thus always the highest possible network priority.

As long as without proper CSS you can't properly display HTML - everything, just everything would wait for it to load first. Let me cite one image from Defer non-critical CSS google article:

CSS blocking render

And there is two way to make this situation better:

  • code split CSS files, so you will need less CSS to download here and now, and could spend less time in networking. Always working, but has some limitations, which might prevent this variant from being adopted in your project. Like it does not worth it.
  • inline "used" or "critical" CSS, which is usually just a few (dozen) selectors required to display content above the fold.

The problem with code-splitting

The problem with CSS and code-splitting is not code-splitting, but CSS. I hope you know what is "rule specificity", when a selector defined "later" has more power than a selector defined before.
Everything was simpler with one big styles.css, but one you shred it into 5 different files - they could be assembled in an absolutely random order, causing all "css related issues" you ever heard about.

to use CSS and code splitting you have to be ready handle your CSS code more responsible, and never rely on the correct rule declaration order between different files.

However - keep in mind - CSS code is usually quite compact, comparing to JS, and does not require much time to download and parse. Code splitting for CSS might not be needed


Critical CSS and CSS

Is something we want. Is something very, well, delightfull? And is yet another problem. To be more concrete - Google Web Fundamentals Guides, in their Inlining critical CSS, are not proposing any feasible solution. Let's move on

According to the Inlining critical CSS there are 3 node libraries, which could help you "extract" a Critical CSS for a given HTML:

penthouse

For given URL and CSS - produce "critical" CSS. Have no idea how I could use it with JAM stack or for SSR.

Uses puppeteer to detect window size, and thus very slow.

critical

A bit more complicated version. But works in the same way.

While both approaches could provide very fine results - their targets are mostly static sites, as long as there is no way you could use them in runtime.

inline-critical and modpagespeed

However, these guys could handle runtime. However, they could just inline .css you will point finger on. That's not a Critical CSS, and, of course, not "extraction".

Ensure that the CSS you are planning to inline is genuinely critical to your web page and you are not inlining everything. source

Used-styles

So - how to obtain the CSS-in-JS level of critical style extraction, but for normal CSS? The thing you will also need for all those css-in-js, which ends in .css, and which I would gently recommend you to use?
Just check which styles were used.

GitHub logo theKashey / used-styles

πŸ“All the critical styles you've used to render a page.

used-styles


Get all the styles, you have used to render a page.
(without any puppeteer involved)

Build Status NPM version

Bundler and framework independent CSS part of SSR-friendly code splitting

Detects used css files from the given HTML, and/or inlines critical styles Supports sync or stream rendering.

Read more about critical style extraction and this library: https://dev.to/thekashey/optimising-css-delivery-57eh

  • πŸš€ Super Fast - no browser, no jsdom, no runtime transformations
  • πŸ’ͺ API - it's no more than an API - integrates with everything
  • 🀝 Works with strings and streams
  • ⏳ Helps preloading for the "real" style files

Works in two modes:

  • πŸš™ inlines style rules required to render given HTML - ideal for the first time visitor
  • πŸ‹οΈβ€β™€οΈinlines style files required to render given HTML - ideal for the second time visitor (and code splitting)

Critical style extraction:

  • 🧱 will all all used styles at the beginning of your page in a string mode
  • …

used-styles are working quite simple - you are giving them you dist directory, they scan it using PostCSS, and thus creating the database of styles might use.

Then you are giving them a piece of HTML - string or stream - and they would tell you which style, from which file, was used. And in which order you shall declare them.

Relax, I am joking - of course, the library would handle everything out of the box, including duplicate prevention. And in case if using React's renderToStream - the result would be interleaved with HTML (however correct rule order would not be guaranteed in this case).

Everything works out of the box, except a few tricky moments...

Tricky moment #1

You still have to load .css files. As long you inlined only a small part of all styles, which might be required just after the page start.

Critical CSS is good for FCP(First Content Paint), but might be bad for the application as a whole.

the inlined CSS will result in some extra page weight every time a user loads your website. For example, if there is 30kB of inline CSS on every page of your website then 10 page views by a single user will cost the user 300kB. It may not sound like a big deal but data is expensive in some parts of the world (and on some 3G/4G data plans). source

Tricky moment #2

Loading these "missing" CSS would be triggered by your bundler automatically, especially in case of code splitting. And those styles would be loaded at maximal network priority, as long as they are CSS, delaying JS start and making hydrating death valley deeper - you have to manually disable, usually by creating a <style data-href="real-style.css" /> to indicate that you handled external style manually.

used-styles would tell you which CSS files was used

Tricky moment #3

Critical CSS is required only for the FIRST visit. Inlining styles second time is a waste of time. You already loaded "real" .css files, and their reloading would not consume any time - everything is cached.
In the ideal situation, you have to track (on SSR) which files you already send to the client, and handle them in the "old" way.

you can control which files should be inlined and which should not

However, this is not so easy and might require some server infrastructure, especially shared cache. (and cookies, at least session cookies)

Would it help much?

That's a great question!

  • Critical CSS Extraction would work only if you have HTML, so you have to have SSR
  • just one file inlining would work any SPA, but it might be not a great idea to inline big files into your HTML - big files are better be cached, as well as inlining small files - they might not change anything - reducing TTFB might have more impact in this case.

Need an example? the urge uses used-styles to make your experience better, however not covering some tricky cases I've listed above.

More to read

and of course πŸ‘‡

Discussion (0)

pic
Editor guide