How to get 100 website performance score

First, let’s see what 100 website performance score means. A tool, called Lighthouse, can be used to measure website performance. The measurement is a score between 0 and 100. 100 means the best performance.

This score is important in several ways:

Lighthouse is integrated into Chrome’s Developer Tools so you can use it from there, but you can also use web.dev or PageSpeed Insights

Here is the performance score of this website using web.dev:

The performance score of this website

The performance score is determined by 3 metrics:

  1. Largest Contentful Paint (LCP): measures loading performance. To provide good user experience, LCP should occur within 2.5 seconds of when the page first starts loading.
  2. First Input Delay (FID): measures interactivity. To provide good user experience, pages should have an FID of less than 100 milliseconds.
  3. Cumulative Layout Shift (CLS): measures visual stability. To provide good user experience, pages should maintain a CLS of less than 0.1.

Why go for 100?

A score of 90+ is great, you are doing a great job. So why would someone want to go for 100?

There is no good answer to this question. There’s not much of a difference between 90+ and 100 scores. People will have the same experience. The rank on Google will be the same. You’ll go for 100 only if you want to demonstrate that you can. This is why I did it anyway.

Why you should not go for it? If your website is very interactive, uses buttons to fetch data, to post data, so is very dynamic, you’ll have a hard time reaching 100 because you’ll need to load too much JavaScript.

How to do it

This can be done by removing a lot of JavaScript, embedding CSS and fonts and using less images and optimizing the images.

1. Remove JavaScript

JavaScript affects LCP & FID.

I’m using Gatsby to build the website, the pages are rendered server-side and served as HTML to the browser. But after the HTML is loaded, it loads 200k of JavaScript, including React. When React loads it adds interactivity to your buttons, fetches data, etc.

Because this website has only links, no buttons, no data fetch, I removed all the JavaScript added by Gatsby using the gatsby-plugin-no-javascript in gatsby-config.js

module.exports = {
  plugins: [...`gatsby-plugin-no-javascript`],
};

I have a button the website, the lightbulb next to the sitename is used to change the theme. That was not working anymore after I’ve removed all the JavaScript, but I’ve solved this by reimplementing the functionality in plain JavaScript inside html.js:

<script
  dangerouslySetInnerHTML={{
    __html: `
        var theme;
        try {
            theme = localStorage.getItem('theme');
        } catch (err) { }

        if(!theme && window.matchMedia('(prefers-color-scheme: dark)') && window.matchMedia('(prefers-color-scheme: dark)').matches) {                
            theme = 'dark'
        }               
        document.body.className = theme || 'light';

        function toggleTheme() {
            var body = document.querySelector('body');
            var newTheme = body.className === 'dark' ? 'light' : 'dark';                
            body.className = newTheme;
            try {
                localStorage.setItem('theme', newTheme);
            } catch (err) { }
        }`,
  }}
/>

Another thing that I did was to call the analytics function from html.js also, this way I don’t include Google Analytics scripts on the website:

<script
  dangerouslySetInnerHTML={{
    __html: `
        // send analytics data
        function sendData() {
            var sitedata = {
                ...
            }
            return fetch('/.netlify/functions/send', {
                body: JSON.stringify(sitedata),
                method: 'POST'
            })
        }
        sendData();
    `,
  }}
/>

If you are using Twitter share on your website you might need to remove the Twitter library and replace the calls with plain links. Here is an example:

<a
  href="https://twitter.com/share?url=https://raresportan.com/how-to-get-100-performance/&amp;text=How%20to%20Get%20100%20Website%20Performance&amp;via=raresportan"
  target="_blank"
  rel="noreferrer"
>
  Please share it with your friends on Twitter
</a>

2. Embed critical styles

CSS files affect CLS. If the CSS is loaded after the HTML is rendered, the page visuals are changing.

Critical CSS must be added inside the HTML using a <style> tag. Do not use a .css file for your critical CSS. Lucky me, Gatsby does this by default. It concatenates the content of all CSS files in a single string that is added as <style> tag inside the HTML.

3. Embed fonts

Just as CSS, fonts affect CLS. The moment the font is loaded, the texts on the page are re-rendered. And just as CSS, the fonts must be in the HTML, and not loaded as separate files.

In my case, I creates a fonts.css that contains the font sources as base64 encoded strings. They end up inside the <style> tag with the rest of the CSS.

@font-face {
    font-family: IBM Plex Sans;
    font-style: normal;
    font-display: swap;
    font-weight: 500;
    src: url(data:font/woff2;base64,d09GMgABAAAAAEjQABEAAAAAy...)

I used a base64 command (available on macOS and Linux) to transform the fonts:

$ base64 myfont.ttf > fontbase64.txt

Alternatively, you can use an online service to do this.

4. Optimize images

You should use few images if possible. If not make sure you optimize the hell out of them. Always set a width and height or put them inside a container that has ‘overflow: hidden’, otherwise when an image is loaded it moved content around and this is very bad for the CLS.

I’m using Gatsby’s plugins to optimize my images. It automatically generates different images for different viewport sizes and loads the images lazy:

<img
  class="gatsby-resp-image-image"
  alt="The performance score of this website"
  title="The performance score of this website"
  src="/static/772422e4c6077575d4fc47afd461bf7e/c5bb3/perf.png"
  srcset="
    /static/772422e4c6077575d4fc47afd461bf7e/04472/perf.png  170w,
    /static/772422e4c6077575d4fc47afd461bf7e/9f933/perf.png  340w,
    /static/772422e4c6077575d4fc47afd461bf7e/c5bb3/perf.png  680w,
    /static/772422e4c6077575d4fc47afd461bf7e/b12f7/perf.png 1020w,
    /static/772422e4c6077575d4fc47afd461bf7e/b5a09/perf.png 1360w,
    /static/772422e4c6077575d4fc47afd461bf7e/eee07/perf.png 1628w
  "
  sizes="(max-width: 680px) 100vw, 680px"
  loading="lazy"
  style="width: 100%; height: 100%; margin: 0px; vertical-align: middle; position: absolute; top: 0px; left: 0px;"
/>

Beside this I’m using a Netlify plugin that optimizes the image even further.

5. Preload pages

One of the nice things Gatsby does is that it preloads all the pages referenced by a specific page. This way navigation from one website page to another is instant.

I loved that. But now that I’ve removed JavaScript, the navigation between pages is much slower. I almost give up at this point. While the initial page load was faster, the in-site navigation was worse.

In the end, I wrote a bit of plain JavaScript that prefetch a page when the user hovers on the link. This way I save 100-300ms and the page are appearing to load faster:

<script
  dangerouslySetInnerHTML={{
    __html: `
        window.addEventListener('DOMContentLoaded', (event) => {
            document.querySelector('button.lightbulb').addEventListener('click', toggleTheme);

            //only in-site links
            var links = document.querySelectorAll('a[href^="/"')
            links.forEach(function(link) {
                link.addEventListener('mouseover', function(e) {
                var l = e.target;
                var href = l.href; 
                var link = document.querySelector('link[href="'+href+'"]');
                if (!link) {
                    var prefetchLink = document.createElement("link");
                    prefetchLink.href = href;
                    prefetchLink.rel = "prefetch";
                    document.head.appendChild(prefetchLink);
                }
            })
        });        
    `,
  }}
/>

Conclusion

If you are willing to make some compromises, most importantly to replace kilos of JavaScript libraries with some vanilla JavaScript you can reach a 100 score in website performance.

While you can do something about CSS and fonts, in most cases is probably not practical to remove all the JavaScript, and a 90+ score is fine.

Want to learn more?