Website Performance Boosting — Part 2 — CSS
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…