How to Improve CSS Performance

Milica Mihajlija

Milica Mihajlija

March 18, 2021

Illustrated by

 Jeffrey Phillips

Combined with the complexity of modern websites and the way browsers process CSS, even a moderate amount of CSS can become a bottleneck for people who deal with constrained devices, network latency, bandwidth, or data limits. Because performance is a vital part of the user experience, it’s essential to make sure you deliver a consistent, high-quality experience across devices of all shapes and sizes and that requires optimising your CSS too.

This post will cover what kinds of performance issues CSS can cause and best practices for crafting CSS that doesn’t get in people’s way.

How does CSS work?

CSS blocks rendering

When there is CSS available for a page, whether it’s inline or an external stylesheet, the browser delays rendering until the CSS is parsed. This is because pages without CSS are often unusable. If a browser showed you a messy page without CSS, then a few moments later snapped into a styled page, the shifting content and sudden visual changes would make a turbulent user experience. That poor user experience has a name–Flash of Unstyled Content (FOUC):

Demonstration of the Flash of Unstyled Content behaviour. Source: CodePen.

CSS can block HTML parsing

Even though the browser won’t display content until it’s done parsing the CSS, it will work through the rest of the HTML. However, scripts block the parser unless they are marked as defer or async. A script can potentially manipulate the page and the rest of the code, so the browser must be careful about when that script executes.

Parser blocking script: how script can block HTML parsing.
Parser blocking script: how script can block HTML parsing.

Because scripts can affect the styles that apply to the page, if the browser is still processing some CSS, it will wait until that’s finished before it runs the script. Since it won’t continue parsing the document until the script has run, that means that CSS is no longer just blocking rendering—depending on the order of external stylesheets and scripts in the document may also stop HTML parsing.

Parser blocking CSS: how CSS can block HTML parsing.
Parser blocking CSS: how CSS can block HTML parsing.

To avoid blocking parsing, deliver the CSS as soon as possible and arrange your resources in optimal order.

Watch the size of CSS

Compress and minify CSS

Establishing a connection to download an external stylesheet inevitably causes latency, but you can speed up the download by minimising the total bytes transferred over the network.

Compressing files can significantly improve speed, and many hosting platforms and CDNs encode assets with compression by default (or you can configure them easily). The most widely used compression format for server and client interactions is Gzip. There’s also Brotli which can provide even better compression results, though it’s not as supported as Gzip.

Minification is the process of removing whitespace and any code that is not necessary. The output is a smaller but perfectly valid code file that the browser can parse and that will save you some bytes. Terser is a popular JavaScript compression tool and if you use webpack, v4 includes a plugin to create minified build files.

Fine-tuning: Remove unused CSS

When using CSS frameworks, it’s relatively common to end up with unused CSS (unless we only include the components we need). The same issue appears in larger codebases that grow over a long time.

Removing unused CSS is often manual work. The main challenge is how complex it is. We have to carefully audit the entire site, in all possible states, on all possible devices (to cover media queries) and execute all JavaScript functionality that might be altering styles. UnusedCSS and PurifyCSS are popular tools that can help pinpoint unnecessary styles, but we should pair them with careful visual regression testing.

Here’s where using CSS-in-JS is a significant advantage: the styles rendered within each component is required CSS only. The secret to fast CSS-in-JS is inlining the CSS into the page or extracting it to an external CSS file. Shipping the CSS in a JavaScript file will cause it to be parsed and evaluated slowly.

Prioritise critical CSS

Critical CSS is a technique that extracts and inlines the CSS for above-the-fold content. Inlining extracted styles in the <head> of the HTML document eliminates the need to make an additional request to fetch these styles and speeds up rendering.

Did you know?
Above-the-fold is all the content a viewer sees on page load before scrolling. There is no universally defined pixel height of what is considered above the fold content since there are many devices and screen sizes.

To minimise the number of roundtrips to first render, keep above-the-fold content under 14 KB (compressed).

Determining the critical CSS is not entirely accurate because you need to make assumptions about the fold position (which varies between device screen sizes). This can be difficult for highly dynamic sites. Even when imprecise, it can still bring performance improvements, and we can automate it with tools such as Critical, CriticalCSS and Penthouse.

Load CSS asynchronously

The rest of the CSS (less critical portion) is best loaded asynchronously. The way to achieve that is by setting the link media attribute to print:

1<link rel="stylesheet" href="non-critical.css" media="print" onload="'all'">

“Print” media type defines the stylesheet rules for when the user tries to print the page, and the browser will load such stylesheet without delaying page rendering. Applying that stylesheet to all media (namely screens and not just print) uses the onload attribute to set the media to all when the stylesheet finishes loading.

Another option is to use <link rel="preload"> (instead of rel="stylesheet") to achieve a similar pattern as above and toggle the rel attribute to stylesheet on load event. There are drawbacks to consider when using this approach.

  • Browser support for preload is still not great, so a polyfill (or using a library such as loadCSS) is necessary to apply a stylesheet across browsers.
  • Preload fetches files very early, at the highest priority, potentially deprioritising other vital downloads.

If you do want the high-priority fetch that preload provides (in browsers that support it), the creators of loadCSS recommend you combine it with the first pattern, like this:

1<link rel="preload" href="/path/to/my.css" as="style">
2<link rel="stylesheet" href="/path/to/my.css" media="print" onload="'all'">

Avoid @import in CSS files

Using @import in CSS files slows down rendering. First, the browser has to download the CSS file to discover the imported resource and then initiate another request to download it before rendering it.

If you have a a stylesheet that contains @import url(imported.css); the network waterfall looks like this:

Network waterfall for imported CSS showing that its download is delayed.

Loading two stylesheets in link elements allows parallel download:

Network waterfall for two CSS files showing that they are downloaded in parallel.

Use efficient CSS animations

When you animate elements on a page, the browser often has to re-calculate their positions and sizes in the document, which triggers layout. For example, if you change an element’s width, any of its children may be affected, and a big part of the page layout might change. Layout is almost always scoped to the entire document, so the larger the layout tree, the longer it performs layout calculations.

When animating elements, it’s essential to minimise layout and repaints. Not all CSS animation techniques are equal and modern browsers can best create performant animations with position, scale, rotation, and opacity:

  • Instead of changing height and width properties, use transform: scale().
  • To move elements around, avoid changing top, right, bottom, or left properties and use transform: translate() instead.
  • If you want to blur the background, consider using a blurred image and changing its opacity.

Fine-tuning: contain property

The contain CSS property tells the browser that the element and its descendants are considered independent of the document tree (as much as possible). It isolates a subtree of a page from the rest. The browser then can optimise the rendering (style, layout, and paint operations) of independent parts of the page to improve performance.

The contain property is useful on pages containing many independent widgets. We can use it to prevent changes within each widget from having side effects outside of the widget’s bounding box. A mostly static site will get little benefit from this strategy.

Optimise font loading with CSS

Avoid invisible text during font loading

Fonts are often large files that take a while to load. Some browsers hide text until the font loads (causing the “flash of invisible text” or FOIT) to deal with this. When optimising for speed, you’ll want to avoid the “flash of invisible text” and show content to people immediately using a system font (one that’s pre-installed on their machine). Once the font file has loaded, it will replace the system font known as the “flash of unstyled text” or FOUT.

One way to achieve this is by using font-display—an API for specifying a font display strategy. Using font-display with the value swap tells the browser that text using this font should be displayed immediately using a system font.

Use variable fonts to reduce the file size

Variable fonts enable many different variations of a typeface to be incorporated into a single file, rather than having a separate font file for every width, weight, or style. They let you access all the variations in a given font file with CSS and a single @font-face reference.

Variable fonts can significantly reduce the file size where you need multiple variants of a font. Instead of loading the regular and bold styles plus their italic versions, you can load a single file containing all of the information.

Monotype ran an experiment by combining 12 input fonts to generate eight weights, across three widths, across both the Italic and Roman styles. Storing 48 individual fonts in a single variable font file meant a 88% reduction in file size.

Don’t worry about the speed of CSS selectors

How CSS selectors are structured affects how fast the browser can match them. The browser reads selectors from right to left, so when you use a descendant selector. For example, nav a {}, it will first match every <a> element on the page before zeroing in on the one that’s inside nav. If you used a more specific selector, for example, .nav-link on each <a> inside the nav element, it wouldn’t spend time trying to match every <a> on the page.

If you consider how browsers match selectors right to left and an example such as .container ul li a { }, you’ll see why descendant selectors are often labelled as “expensive”.

It might seem that such selectors would be a speed problem. However, the selector matching performance is fast. The CSS declarations are so friendly to compression algorithms that the effort required to optimise a CSS selector is usually better spent working on other parts of your application with a greater return on investment.

CSS is critical to loading pages and a delightful user experience. While we often might prioritise other assets (such as script or imagery) as more impactful, we shouldn’t forget CSS. With the strategies described above, you will be able to ensure fast delivery and execution.

Milica Mihajlija

Milica Mihajlija

Web Developer and Technical Writer. Find her on Twitter.

Become a site speed expert

Join thousands of subscribers keeping up-to-date with web performance news and advice.

The best performance newsletter I’ve come across. Highly recommended.

Addy Osmani

Addy Osmani

Engineering Manager at Google Chrome