Website Performance Boosting — Part 2 — CSS

Ralf
REWRITE TECH by diconium
9 min readJan 25, 2023

The second part of my “Performance Boosting” series is all about optimizations on your website’s page speed that can be carried out using CSS.

Avoid layout shifts

When building a website, some content may load faster than others. This can quickly lead to layout shifts. Annoyed users know the effect very well:

You enter a website and want to click a button. The moment you want to click the button, something jumps in the layout and in the worst case, you’ve clicked on an ad and leave the website — usually to a place you don’t want to be.

Google captures these delays in page loading in the Cumulative Layout Shifts (CLS) metric. To improve this value we can do the following:

Avoid layout shifts caused by images

To use the correct spacing, specify dimensions (width and height) for images and videos.

If you use responsive design, you will find that it is often not possible to work with fixed or specified dimensions. The height in particular will become a problem, since the aspect ratio of the images often needs to be maintained, with square images for example.

Since percentage padding is calculated relative to the width of the element, we can use a very simple trick to calculate a suitable height: For this square example, the “padding-top” must be given the value “100%” so that width and height will be equal.

Check out this example for responsive square images and images in 16:9 format:

Use “object-fit: cover”

These examples above will only work if you have the option to use background images via CSS. One way of using images in a responsive layout directly (<img> tag) in the HTML and not triggering layout shifts would be to use a fixed height.

With the CSS property “object-fit” and the value “cover”, the image fills the entire available space. In this case, the width of the image will adapt to the screen, but the height remains the same. A good use for this is layout cards which is often displayed in search results. Here’s an example:

The disadvantage of using “object-fit: cover” is that part of the image usually has to be cut off…

Avoid layout shifts caused by ads, embeds, and iframes:

  • Reserve ad slot size (preferably the largest) before loading the ad script (To serve ads as fast as possible consider preloading your ad script).
  • Move your ads out of the viewport which is first visible for the user.
  • Use placeholders when there is no ad available or the ad is still loading.

Avoid layout shifts caused by fonts

When webfonts are used, you will notice when the page loads that a default system font is used for a short time until the webfont has downloaded and the text is re-rendered. The display of the font and the associated font sizes and line spacings can lead to shifts in the layout since the new height of the outer container has to be recalculated.

To avoid the so called “flash of unstyled text” (FOUT), you can preload required web fonts immediately. Add the following <link> for your font file inside the head of your HTML code:

<head>
<link rel="preload" href="/fonts/Open-Sans.woff2" as="font" type="font/woff2" crossorigin>
</head>

Your webpage will only be rendered when your font file is preloaded and ready to use.

In part one of my Performance Boosting series, I already described a way to avoid layout shifts caused by web fonts by using a Base64 encoded font file inserted inside the <head> section.

Avoid layout shifts caused by non-composited animations

Non-composited animations cause a lot of calculation — they have to calculate the layout, do repaints and cause composition operations. You can read more about non-composited animations in the animation section of this article.

For example, animating the “top” and “left” values of absolutely positioned elements triggers layout shifts even though they are actually outside of the normal layout flow and don’t appear to affect other elements. Instead you can use “transform: translateX()” and “transform: translateY()” to animate the positioning without causing layout shifts.

Check the complete list of CSS properties which may cause or not cause a repaint of pixels at the bottom of this page in the resources section.

Media queries for background-images

To save your users long loading times, especially for mobile users, you can create background images in different sizes and assign these images to the appropriate element using media queries:

Mobile devices:

@media (max-width: 480px) {
.img {
background-image: url(img/bg-mobile.jpg);
}
}

Tablets:

@media (min-width: 481px) and (max-width: 1024px) {
.img {
background-image: url(img/bg-tablet.jpg);
}
}

Desktop devices:

@media (min-width: 1025px) {
.img {
background-image: url(img/bg-desktop.jpg);
}
}

Preload resources

In the section “Avoid layout shifts caused by ads, embeds, and iframes” I already mentioned that it is advisable to preload ad scripts in order to be able to display the ads as quickly as possible to increase revenue. To do this, place the following line in the <head> of your document, for example to deliver fonts, important scripts for interaction or other resources to the user more quickly.

<link rel="preload" href="style.css" as="style" />
<link rel="preload" href="script.js" as="script" />

Critical CSS

CSS files must be downloaded, interpreted, and later rendered by the browser. It should also be noted that even if CSS files have already been downloaded and cached, all rules still have to be interpreted by the browser — and this with every page change. The more rules that exist and the more complex the rules are, the longer it takes the browser to load the website. So first remove CSS rules that are never used from your code.

To improve Google’s First Contentful Paint (FCP) metric and solve the “Eliminate render-blocking resource” issue in Lighthouse audits you should consider to splitting up your CSS into critical and non-critical CSS code:

Extract critical CSS

To avoid render-blocking resources, the CSS code, which is needed to display the first visible area of the website on the screen, should be extracted and inserted inline into the <head> area.

Defer non-critical CSS

All other CSS rules that are not required for the first visible area can be loaded afterwards. For this, the rules can be packed in an external CSS file and can then be loaded deferred or asynchronous.

The following method is recommended by Google to reload non-critical CSS. Here, the <link> element is converted into a stylesheet after the page is fully loaded by adding the attribute “rel=stylesheet”. You can simultaneously preload the CSS code with the attributes rel=”preload” and as=”style”:

<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

Efficient CSS

Use efficient selectors

Use simple selectors — the best are classes or IDs — and try to avoid nesting. The browser will attempt to locate each element in its parent container, even if those elements aren’t even on the page.
It is best to always address the direct element. The more complex and specific the selector becomes, the more computation is required.

Browsers search the elements from your rule from right to left. In the following example, the browser will look for all elements with the class “.button” in the document, then all elements with the class “.teaser” and so on. In this example, five elements are searched for in the DOM. If you address elements directly, however, only one element is searched for and cached. It gets even worse when HTML elements are addressed directly, such as “.wrap .content h2 {…}”. In this case, all “h2” elements that are in the document are cached.

/* not nice */
body .wrap .content .teaser .button { /* too much nesting */
color: #fff;
}
/* nice */
.button { /* the exact element needed */
color: #fff;
}
/* not nice */
aside#sidebar .description { /* too specific */
color: #fff;
}
/* nice */
#sidebar {
container-type: inline-size;
container-name: sidebar;
}
@container sidebar (min-width: 700px) { /* usage of CSS containers */
.description {
color: #fff;
}
}

If you use a preprocessor like Sass you will quickly be tempted to create a lot of nesting. The generated rules can then also have the nesting. Let’s see what we can do about it with the given HTML code:

<div class="block block--mod">...</div>
<div class="block block--size-big">...</div>

Let’s create a lot of nesting :)

/* not nice */
.block {
color: #fff;
background: #000;

.block--mod {
color: #000;
background: #fff;
}

.block--size-small {
font-size: 1.2em;
}

.block--size-big {
font-size: 2em;

.block--size-big-headline {
font-size: 3em;
}
}
}

/* output will be */
.block {
color: #fff;
background: #000;
}

.block .block--mod {
color: #000;
background: #fff;
}

.block .block--size-small {
font-size: 1.2em;
}

.block .block--size-big {
font-size: 2em;
}

.block .block--size-big .block--size-big-headline { /* It can get even worse */
font-size: 2em;
}

To avoid unnecessary nesting, the & feature of Sass can be used, which is particularly worthwhile when using BEM:

/* nice */
.block {
color: #fff;
background: #000;

&--mod {
color: #000;
background: #fff;
}

&--size {
&-small {
font-size: 1.2em;
}

&-big {
font-size: 2em;
}
}
}

/* output will be */
.block {
color: #fff;
background: #000;
}

.block--mod {
color: #000;
background: #fff;
}

.block--size-small {
font-size: 1.2em;
}

.block--size-big {
font-size: 2em;
}

Animations

Web animations that are implemented using CSS or JavaScript can result in a recalculation of the visible pixels. This can cause longer loading times and layout shifts. The calculations of the animations play blocks the so-called main-thread, which in turn delays the time for user interactivity and can delay other important tasks.

There are two kinds of animations: composited ones and non-composited. The latter are the bad ones. They require layout operations, repaints, and composition calculations.

Composited animations — aka the good ones (i.e., opacity, transform)— work on a separate thread and will not trigger re-paintings. Look at the list of CSS triggers below to see which CSS properties require which operations.

Use GPU for CSS animations and transitions

By using “translate3d(x, y, z)” or “translateZ(z)” you can tell your browser to choose hardware acceleration for animation calculation purposes. You will get fast and smooth animations (60 frames / second).

transform: translate3d(0,0,0);

or

transform: translateZ(1);

GPU accelerated animations can be used for the following types:

  • 3D transforms (CSS)
  • Transitions (CSS)
  • Canvas (JS)
  • WebGL 3D (JS)

In a following part of this series, I will deal with efficient animations using JavaScript via RequestAnimationFrame.

Read more about the pros and cons of GPU animation in the useful links section below.

Useful links

Aspect Ratio Boxes

Preload fonts

Complete list of CSS properties that may or may not trigger repainting of pixels

Defer non-critical CSS

Extract critical CSS

npm module for critical CSS

Preload resources

GPU accelerated animations

CSS Container Queries

Part one of my “Performance Boosting” series

So, what’s next?

In the following parts of my series I will focus on DOM, JavaScript and some server optimizations.

Stay tuned…

--

--