The aim of this post is to examine how frontend developers build fast landing pages, as judged by Core Web Vitals.
These webpage performance metrics are a new ranking signal for Google Search and are a concern for digital marketers and SEO professionals. Since Google announced them in May 2000, business owners asked frontend developers to fix slow pages and build faster ones.
Now that Core Web Vitals has started rolling out, I went looking for the best frontend performance patterns.
What are Core Web Vitals?
The topic of Core Web Vitals includes three user-centric, webpage performance metrics derived from real Chrome user data (CrUX). The scores are aggregated to the 75th percentile over a rolling 28-day period. The three metrics are:
- Largest Contentful Paint (LCP): the time it takes for a website to display the largest element on the screen. It should be seen by the user in under 2.5 seconds
- First Input Delay (FID): this measures the time it takes for a webpage to respond to user input, which should be within 100 milliseconds
- Cumulative Layout Shift (CLS): this is the unexpected shift of webpage elements and layout as the page loads, so it’s best to avoid shifting the layout of the page during load
It is critical to measure your pages against users with the slowest connections and devices.
So, I throttle my connection to fast 3G and slow down my CPU by 6x. Only then am I able to surface issues reported by Google’s Search Console.
Improving your Core Web Vitals score
The secret to fixing page speed problems is to reverse engineer pages that already have good Vitals. First, I needed a page type that is common to most sites, so, for the purpose of this article, I chose to measure the ubiquitous pricing page because they are visually similar across websites and generally include a prominent call to action.
Then, I tested the 114 mobile pages with Lighthouse and compared them against Core Web Vitals. This left me with 25 fast landing pages to analyze.
Technologies on these sites ranged from Next.js to WordPress to Ruby on Rails, and some even used jQuery. No matter what stack you (are forced to) use, there is always a way to improve your Core Web Vital scores.
As these patterns are reverse-engineered from existing pages using data from the Google Chrome User Experience Report (CrUX), I am confident that if you follow them, you will build a fast webpage, or even fix a slow one.
To investigate these patterns on your own, I recommend loading the page, then viewing the source code.
Keep backend times under 600ms
The fastest 10 sites had server responses ranging from 75ms to 380ms with a 110ms median. The takeaway? The 75th percentile from the field probably needs to be consistently under 600ms.
Calculate a performance budget
Before looking at code, I recommend performing an inventory of the files requested and breaking them down by content type. Then compare your page to that of a fast page. This is called a performance budget.
I created my budget by breaking down each type of file that loads prior to the largest contentful paint (LCP). For example, Shopify loads over 100 files but built the page so that it loads 17 files before the largest contentful paint.
Before LCP, fast pages generally load:
- Fewer than 35 total files
- Four or fewer third-party scripts
- No media files
- Only two stylesheets
- No more than 10 scripts
- Up to four custom fonts
- As many as 15 images
Preconnect to requested domains before LCP
Properly built pages start by finding all the domains used by these 35 files and then use the
preconnect resource hint. Shopify uses this pattern to preconnect to critical domains:
<head> <link rel="preconnect" href="https://cdn.shopify.com" /> <link rel="preconnect" href="https://monorail-edge.shopifysvc.com" /> <link rel="preconnect" href="https://sessions.bugsnag.com" /> <link rel="preconnect" href="https://www.google-analytics.com" /> <link rel="preconnect" href="https://www.googletagmanager.com" /> <link rel="preconnect" href="https://bat.bing.com" /> <link rel="preconnect" href="https://www.facebook.com" /> <link rel="preconnect" href="https://connect.facebook.net" /> <link rel="preconnect" href="https://tags.tiqcdn.com" /> <link rel="preconnect" href="https://lux.speedcurve.com" /> </head>
This allows the browser to open connections before files are requested.
Preload and host font files
Most sites rely on custom fonts and use
font-display: swap so that text is visible until the custom font is downloaded. PosterMyWall is an example of the pattern where the font is preloaded and self-hosted. This is the fastest way to load and render custom fonts.
<head> <link rel="preload" href="https://www.postermywall.com/assets/fonts/NunitoSans/NunitoSans-R egular.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <!-- Suggested improvement → <link rel="preload" as="style" href="https://pro.fontawesome.com/releases/v5.13.0/css/all.css" onload=”this.rel='stylesheet'"> </head>
If you need to use Google Fonts, you should look to use Mario Ranftl’s google-web-fonts-helper.
Avoid icon fonts
The main reason to avoid icon fonts is that they create a request chain. This means that the root document has to load a stylesheet, which in turn loads the icon font file before it can render the icon. But a simple image with a height and width attribute has no dependencies.
If you need to use an icon font, Sproutsocial outlines a useful pattern when it preloads the font and asynchronously loads it. I would additionally recommend preloading the icon font files as well.
<head> <link rel="preload" as="style" href="https://pro.fontawesome.com/releases/v5.13.0/css/all.css" onload=”this.rel='stylesheet'"> <!-- My recommendation --> <link rel="preload" href="https://pro.fontawesome.com/releases/v5.13.0/webfonts/fa-so lid-900.woff2" as="font" type="font/woff2" crossorigin="anonymous"> </head>
I have also seen the benefit of adding
@fontface rules inline as well, which removes the render-blocking
Inlining critical CSS styles is not required
I was surprised to discover that many sites do not inline styles. They instead create small bundles of CSS to load in the
<head>. For example, Loom preloads external stylesheets that total about 15KB.
That said, Amazon AWS loads a single 74KB render-blocking CSS file. So, don’t just use a pattern because Lighthouse or a post (like this) recommends it.
<head> <link rel="preload" href="/_next/static/css/4971ebe5c5a60f5f989b.css" as="style" /> <link rel="stylesheet" href="/_next/static/css/4971ebe5c5a60f5f989b.css"/> <link rel="preload" href="/_next/static/css/33a02aaca547371a75e0.css" as="style" /> <link rel="stylesheet" href="/_next/static/css/33a02aaca547371a75e0.css"/> </head>
My budget recommends no more than 10 scripts before the largest contentful paint. This is not permission to load 10 massive files, but the best sites load small scripts (2 to 35KB) at the bottom of the page intelligently applying
If you have a different technology stack when you are preloading scripts, pay attention to resource ordering.
Use inline SVG for small images and lazy load the rest
If you have images that can be relatively small SVGs, inline them. Trello inlines all the SVG images above the fold and then lazy loads those that are offscreen or hidden.
I also have used
loading=eager for images above the fold. Sites like Trello use the native
<body> <svg height="16" viewBox="0 0 16 16" width="16" <path>...</path> </svg> <!-- Native --> <img src="//images.ctfassets.net/TREL-210_Trello_Access_Spot_Illo.svg" alt="Atlassian Access" width="271" height="190" loading="lazy" class="Picture__Image-sc" /> <!-- Script --> <img src="https://www.postermywall.com/assets/.../placeholder-image.png” data-src="https://d1csarkz8obe9u.cloudfront.net/.../green-restaurant.jpg" class="pmw-lazy" alt="Blog post" /> </body>
<picture> element to display a large hero image
None of the pages analyzed included a hero image component, so I searched and found Trulia, which employed a novel pattern. To start, they set a PerformanceMark, which indicates to me a high degree of intentionally.
Risking oversimplification, I observed that for the large hero image, the developers used the
<picture> element containing 19
<source> elements covering all types of use cases. Not only is the image sized and compressed correctly for desktop and mobile, but they take advantage of the
<picture> art direction capabilities. This tells me also that the UX team was part of this solution.
<picture class="Picture__PictureContainer-sc-1exw3ow-1 gteZiU"> <source srcSet="https://www.trulia.com/images/app-shopping/homePage/extraLarge.jpg" media="(min-width:993px)" /> <source srcSet="https://www.trulia.com/images/app-shopping/homePage/medium.jpg" media="(min-width:768px)" /> <source srcSet="https://www.trulia.com/images/app-shopping/homePage/medium.jpg" media="(min-width:570px)" /> <source srcSet="https://www.trulia.com/images/app-shopping/homePage/small.jpg" media="(min-width:376px)" /> <source srcSet="https://www.trulia.com/images/app-shopping/homePage/small.jpg" media="(min-width:0px)" /> <img loading="auto" decoding="auto" id="homepage-banner-image" style="background:url(src="data:image/image/png;base64,iVBOR=="); background-size:cover" width="100%" height="100%" src="https://www.trulia.com/images/app-shopping/homePage/extraLarge.jpg" alt="" class="Picture__Image-sc-1exw3ow-0 tzLdz" /> </picture>
And, to emulate a background image, they use the
position property to layer several
<div> elements to complete the component. The result is amazing.
The secret to image painting is ingenious.
<img> element has an inline style setting, a base 64 encoded, and a 10px by 5px JPG as a background image with the height and width set to 100%. This is why it paints so quickly and has a nice effect that makes the image go from blurry to crystal clear. Wow.
Load third-party scripts when
<readyState> is interactive
Managing third-party vendors is challenging, especially when the budget recommends only four scripts be loaded before LCP. The business can still use third-party vendors despite this, as the pages analyzed requested an average of 103 third-party files be fully loaded. So I set out to see how the best pages dealt with vendor scripts.
The best pattern loaded up to four scripts (i.e., tag manager, AB testing, RUM) before
readyState is interactive, then loaded the remaining after
<head> <link rel="preconnect" href="https://assets.adobedtm.com" /> </head> <body> <!-- Bottom of HTML → <script src="https://assets.adobedtm.com/launch-EN.min.js" async></script> <! -- Self-host --> <script src="https://cdn.shopify.com/speedcurve-lux/js/lux.js?id=64441087" async defer crossorigin="anonymous"></script> </body>
If you employ a tag manager, the waterfall ought to be expected. It loads critical third parties before
DOMContentLoaded, including your tag manager script. Then, set the browser event trigger to Window Loaded for all your remaining scripts.
This describes the best pattern. However, your situation may require something less aggressive. The takeaway is to take control over when your scripts are being loaded, not just for vendors, but for all scripts in order to optimize the waterfall.
Data-driven patterns to improve Core Web Vitals
Additionally, I would not expect these high-quality sites to fail Cumulative Layout Shift. But, I think I would find more CSS patterns if I analyzed ad-supported articles instead of pricing pages.
This analysis is not a surprise. All of these patterns are well documented, with the exception of using the picture element for a large hero image.
But if you are choosing a solution, I would recommend using these patterns, which are based on case studies rather than recommendations listed by Lighthouse.