Web Vitals: how to speed up your website (and improve PageSpeed score)
In this post, we will see a fundamental concept when you are making a modern website: Web Vitals.
If you want to achieve a better user experience and a better score on tools such as PageSpeed, you need to optimize three fundamental UX aspects:
Loading performance;
Interactivity;
Visual stability.
Every aspect has a correspondent metric, we’ll see them in detail in the next section. You can measure these metrics using PageSpeed or Web Vitals extension.
Metrics
Loading performance — Largest Contentful Paint (aka LCP)
Largest Contentful Paint computes the time between the start of the page load and the time when the largest element in the viewport (aka visible screen when you open a page) become visible. If the element isn’t in the viewport, it is not considered.
Another used metric is the First Contentful Paint, so the time when the first element in the viewport is added to DOM.
HTML elements considered in LCP
Images (also inside SVG);
Poster image in a video;
URL within a background image;
Elements containing text.
✅ Good value: less than 2.5s
👎 Poor value: more than 4s
Interactivity — First Input Delay (aka FID)
Measures the time from when a user first interacts with a page to the time when the browser can handle it. Typically, the main cause is a heavy JS execution.
✅ Good value: less than 100ms
👎 Poor value: more than 300ms
Visual Stability — Cumulative Layout Shift (aka CLS)
Measure the amount of unexpected movement of content on a web page.
In fact, it is the largest layout shift score, calculated as follows:
layout shift score = impact fraction * distance fraction
The impact fraction is the union of all visible areas of elements that change position. The distance fraction is the greatest distance made by any of these elements. A layout shift happens after an interaction, it is not calculated.
✅ Good value: less than 0.1
👎 Poor value: more than 0.25
Optimize CSS
Inline critical CSS — LCP
If you have a render-blocking stylesheet, you can inline it into the HTML to avoid an additional network request using the style tag. Do this only if the stylesheet is small!
<head>
<style>.btn { color: #ff0000; }</style>
...
</head>
Preload CSS — LCP
If you need an important CSS as soon as possible, you can tell it to the browser using rel="preload" on the link tag.
<link rel="preload" as="style" href="style.css" />
Defer non-critical CSS — LCP, CLS
If you have a CSS which don’t provide a critical impact on your page (especially if the related elements are not in the viewport), you can defer its loading to optimize LCP. In this way, the style will be loaded asynchronously.
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
Minify CSS — LCP
Minify the CSS using tools such as CSS Minifier.
// non minified
.btn {
color: black;
}
// minified
.btn{color:black;}
Optimize CSS background images with media queries — LCP
You can change the background image using media queries. In this way, you can load the smallest files for mobile and tablet, reducing the file size.
// mobile
@media (max-width: 480px) {
body {
background-image: url("mobile.jpg");
}
}
// tablet
@media (min-width: 481px) and (max-width: 1024px) {
body {
background-image: url("tablet.jpg");
}
}
// desktop
@media (min-width: 1025px) {
body {
background-image: url("desktop.jpg");
}
}
Optimize JS
Inline critical scripts — LCP, FID
If you have a render-blocking script, you can add it to the HTML page using the script tag. Do this only if these scripts are really small.
<body>
...
<script>
alert("I'm a critical script!")
</script>
</body>
Split the bundle and lazy load chunks on demand — LCP, FID
When importing a module you will not use it when the page is loaded, you can import it when a call that uses the module will be made.
You can rewrite a function you don’t need immediately, like this
import module1 from "library";
form.addEventListener("submit", e => {
e.preventDefault();
someFunction();
});
const someFunction = () => {
// function using the module1
}
Into this
form.addEventListener("submit", e => {
e.preventDefault();
import('library.module1')
.then(module => module.default) // using the default export
.then(() => someFunction())
.catch(handleError());
});
const someFunction = () => {
// uses module1
}
Reduce the impact of third-party code — FID
You can defer the script execution as in the next advice to avoid render-blocking JS. I suggest searching online for each third-party code because the ways to reduce their impact are different between any script.
Defer scripts — FID
You can use the defer attribute on the script tag to load the external script after the page has loaded.
<body>
...
<script src="..." defer></script>
</body>
Minimize main thread work
You can use a setTimeout function to execute non-critical scripts when the main thread doesn’t have other tasks.
function saveSettings () {
// critical work user visible
validateForm();
showSpinner();
updateUI();
setTimeout(() => {
// non critical work for user
saveToDatabase();
sendAnalytics();
}, 0);
}
Use a web worker to run JS on a background thread (approfondire)
If you want to execute JS on a background thread (i.e. not in the main thread, to avoid blocking scripts), you can use a Web Worker.
If you are new to this topic, I suggest this simple tutorial. If you want to have an advanced detail, you can read this: Web Workers API.
Optimize images
Images without dimensions — CLS
Initially, images without width and height attributes are loaded as 0x0px. So, when the image will be rendered, it will take its real size and will shift the layout. So, include width and height or use aspect ratio.
<img src="image.webp" width="150" height="200" />
Preload images with high priority— LCP
If an image should be loaded as soon as possible, for example, the hero image or the logo in the navbar, you can tell the browser that they should be preloaded.
<img rel="preload" fetchpriority="high" src="image.webp" />
Lazy load on images — LCP
Using loading="lazy" on an image will defer its loading until the visitor reaches a certain distance from the viewport. Use this only if it isn’t visible in the viewport when a user opens the page.
<img src="image.png" loading="lazy" alt="…" width="200" height="200" />
You can do it also in thepicture tag!
<picture>
<source media="(min-width: 800px)" srcset="large.jpg 1x, larger.jpg 2x">
<img src="photo.jpg" loading="lazy">
</picture>
Serve responsive images — LCP
<img src="flower-large.jpg" srcset="flower-small.jpg 480w, flower-large.jpg 1080w" sizes="50vw">
Choose the right image format — LCP
We can aggregate images into two main formats:
Vector for simple geometric shapes (for example SVG)
Raster for complicated images such as photos(for example PNG, JPEG, WebP)
To preserve fine details with high resolution use PNG or lossless WebP.
To optimize photos or screenshots use JPEG or lossy WebP.
Advice: Instead of gif, use video element. You can optimize videos by using FFmpeg.
Choose the correct level of compression — LCP
Use tools like compressor.io or imagemin to reduce the image size.
There are two types of compression:
Lossy: eliminates some pixel data (suggested for photos);
Lossless: filter compresses pixel data (suggested to preserve fine details).
Remove unnecessary metadata
You can remove image metadata such as geolocation using tools like this. I suggest this for privacy reasons and also to reduce a bit the file size.
Resize images
If not needed, you shouldn’t use images of the correct size. This is trivial, but many websites have 5000x5000px images used only in areas having at most 300x300px.
Use WebP images — LCP
A WebP image is typically smaller than a PNG or JPEG, so use it when possible. Not every browser supports it (list), so I suggest using it in a picture tag with a JPEG (or PNG) fallback.
<picture>
<source type="image/webp" srcset="flower.webp">
<source type="image/jpeg" srcset="flower.jpg">
<img src="flower.jpg" alt="">
</picture>
Optimize assets
Resource load time — LCP
Reduce the size of the resource using already said techniques and use modern formats such as WebP and WOFF2. Furthermore, you can reduce the distance the resource has to travel using a CDN.
General optimizations
Ads without dimensions — CLS
Reserve space for the slot and avoid collapsing the reserved space if there is no ad returned. In this way, you delete the layout shift.
Embed and iframes without dimensions — CLS
Use a placeholder precomputing space for embeds and insert a placeholder. Use proper width and height attributes also in this case.
Dynamically injected content — CLS
Avoid inserting new content above existing content. Instead, reserve space in the viewport using a placeholder.
Optimize WebFonts
Remember to use WOFF2 format instead of other formats, because it is optimized for the Web.
Use unicode-range to declare which characters a font can be used for
When you use font-face, you can tell also the range of characters used.
@font-face {
font-family: "Open Sans";
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
unicode-range: U+0025-00FF;
}
Use font-display attribute – LCP, FCP, CLS
optional: performance is a top priority
swap: to ensure web-font is used
block: priority is to display the text
@font-face {
font-family: "Open Sans";
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
font-display: optional;
}
Preconnect to 3rd party origins
If you are using fonts from 3rd party, you can tell the browser to connect as soon as possible to the site.
<link rel="preconnect" href="https://fonts.com">
If you’re interested in this topic, read more on web.dev. It deep dives into this topic and provides many advanced examples.