Kevin Farrugia https://imkev.dev/ Frontend engineering and web performance, by Kevin Farrugia Tue, 15 Apr 2025 11:00:00 GMT https://validator.w3.org/feed/docs/rss2.html imkev.dev en 60 Kevin Farrugia https://imkev.dev/media/logo.jpg https://imkev.dev/ Copyright 2023, Kevin Farrugia Web Performance Frontend Engineering Technology Web Development <![CDATA[Adding a CrUX Vis shortcut to Chrome's address bar]]> https://imkev.dev/crux-vis-shortcut https://imkev.dev/crux-vis-shortcut Tue, 15 Apr 2025 11:00:00 GMT CrUX Vis is a (experimental¹) tool that allows you to visualize and analyze the Chrome User Experience Report (CrUX) data. The CrUX dataset provides insights from real users, including loading performance, responsiveness, and visual stability.

Screenshot of CrUX Vis showing the CWV for web.dev

The preceding image is taken from CrUX Vis and shows the Core Web Vitals of https://web.dev.

Accessing the CrUX Vis report is straightforward as you are only required to input the URL (origin or page). However, if you are as lazy as me you can speed this up slightly by adding a Chrome search shortcut. This was my preferred method for accessing the CrUX Dashboard and I wanted to replicate this behavior for CrUX Vis.²

To create a custom Search Engine in Chrome, you need to navigate to chrome://settings/search and select Manage search engines and site search.

Screenshot of Chrome's Search settings

Find the section titled Site Search and click on the Add button. In the dialog, insert the following details:

  • Search engine: CrUX Vis
  • Shortcut: cruxvis (or any other unique keyword)
  • URL with %s in place of query: https://cruxvis.withgoogle.com/#/?view=cwvsummary&url=https%3A%2F%2F%s&identifier=origin&device=ALL&periodStart=0&periodEnd=-1&display=p75s

Screenshot of Chrome's Add Site Search

You can now navigate to a CrUX Vis report directly from Chrome’s address bar. Typing cruxvis and pressing tab in the address bar will allow you to input the URL of the origin or page you want to query and Chrome will navigate to the CrUX Vis report for that URL.

Screenshot of Chrome's Address bar with CrUX Vis highlighted

Note that the URL should not include the protocol as https is inferred through the URL we used when creating the custom search.

Thank you for reading and I hope you’ll find this useful.


¹ CrUX Vis is expected to replace the CrUX Dashboard in the coming months. If you have any feedback on CrUX Vis, let the CrUX team know using the CrUX Vis survey or the CrUX discussion group.

² This post is heavily inspired by Rick Viscomi’s CrUX Dash shortcut. ❤️

]]>
Web performance
<![CDATA[Improving Largest Contentful Paint on slower devices]]> https://imkev.dev/bimodal-distributions https://imkev.dev/bimodal-distributions Sat, 09 Mar 2024 17:00:00 GMT Looking at our real user metrics, we noticed that our histogram followed a bimodal distribution. A multimodal distribution has more than one mode (a.k.a. peak) and a bimodal distribution has two. Segmenting the data into two groups and optimizing each one separately allowed us to improve our website’s Largest Contentful Paint for most of our users.

Histogram showing Largest Contentful Paint and Page Views. The histogram has a bimodal distribution

The histogram above is taken from January and the two modes are highlighted in red. After careful examination of the data available, we used SpeedCurve to segment our data by device memory. The API has some minor limitations due to privacy, but it allowed us to correlate performance with the user’s device. We could also see that almost all of our users are evenly split between 8 GB and 4 GB devices, with very small numbers for 2 GB and 1 GB devices.

Unsurprisingly, devices with more RAM—and most likely better CPUs—perform better. What was more surprising was the huge difference in loading performance between these user segments.

Horizontal bar chart comparing the Largest Contentful Paint grouped by device memory

As illustrated in the preceding chart, in January, users with 4 GB devices had a loading experience that was almost 2 seconds slower when compared to 8 GB devices. The performance continued to degrade even further on 2 GB and 1 GB devices.

We identified several issues that were contributing to the slow loading performance users were experiencing on low-end devices. I will be writing about these in more detail in a future blog post, but in a nutshell, the majority of issues were CSS-related.

Screenshot from Chrome DevTools showing a 200-millisecond delay labeled Recalculate style

For example, on interaction with one button, we would add touch-action: none to the <body> tag. Surprisingly, this affected all DOM elements (6,700 elements in the screenshot above), causing style recalculations—that while negligible on a high-end device—were blocking the main thread for more than 200 milliseconds on low-end phones.

Over two months, we improved LCP on 4 GB devices to 3.4 seconds, reducing the gap with 8 GB devices.

Horizontal bar chart showing the improved LCP from January to March

The preceding chart shows that LCP on 8 GB devices improved from 2.88 seconds to 2.30 seconds (a 580-millisecond improvement), while performance on 4 GB devices improved from 4.50 seconds to 3.42 seconds (a 1,080-millisecond improvement).

Conclusion

It is useful to recognize if your data consists of two or more distinct groups of users. If your histogram has two modes, understanding and recognizing why you have two groups can be a powerful tool for improving your website’s performance.

In this example, the two groups contained users with different hardware specifications. In other examples, groups may arise from different geographical locations, user behavior, or maybe one group is irrelevant to you. In any case, understanding the differences between these groups of users helps you improve the user experience for all of them.

Thank you for reading. Feedback is always welcome.

Notes

  • The website is a single-page app built in React. It is dependent on JavaScript for almost anything, including rendering critical content and—in most cases—the LCP element. Fixing that is a story for another day.
  • All charts included in this post show data from the home page on mobile devices.
  • The data is sampled and was collected using SpeedCurve. Visualizations are my own.
  • An export of the data is available on Google Sheets.
]]>
Web performance
<![CDATA[Learn Performance course on web.dev]]> https://imkev.dev/learn-performance https://imkev.dev/learn-performance Wed, 01 Nov 2023 12:00:00 GMT Together with Jeremy Wagner, I am proud to have co-authored the web.dev Learn Performance course.

The course covers introductory web performance topics, including resource hints and image performance, before diving into more advanced topics, such as JavaScript scheduling, prefetching and prerendering, and web workers.

Thank you to Rachel Andrew and Barry Pollard for their invaluable reviews. Also, a shout-out to the entire web performance community for their continuous feedback and help. 🙏

The course is in its early phases. If you have discovered any bugs or would like to provide feedback or content suggestions, you are welcome.

]]>
Web performance Tutorials
<![CDATA[Setting up a Private WebPageTest instance]]> https://imkev.dev/private-wpt https://imkev.dev/private-wpt Mon, 26 Jun 2023 12:00:00 GMT This document explains the steps which were taken to set up a private WPT instance on an Ubuntu 22.04 Server.

Virtual Machine

The recommended approach is to set up WPT on a virtual machine rather than your machine. This could be an EC2 machine or a virtual machine. In this tutorial, I am using virt-manager on an Ubuntu host.

When setting up virt-manager, it is recommended to enable File System passthrough.

  1. Enable Shared Memory
  2. Setup a File System passthrough using virtiofs
  3. After installation is complete configure it to automatically mount the shared drive:
$ sudo nano /etc/fstab

Add the following line:

QEMU /mnt virtiofs rw,_netdev 0 0

You may also need to enable IP forwarding on your host machine if it is disabled

sudo sysctl -w net.ipv4.ip_forward=1
sudo systemctl restart libvirtd

WPT Server

The first step is to set up a WebPageTest server. The WebPageTest server hosts the PHP files which are used to show the user the WPT interface as well as run PHP scripts.

webpagetest has the following branches:

  • master - used on www, not for commercial use (recommended for personal use)
  • release - rebased from master (might be outdated)
  • apache - uses the Apache license. Can be used for commercial purposes

To install the WebPageTest server, you can use the install script provided by webpagetest. The below script is taken from wptserver-install but uses the master branch

#!/bin/bash

#**************************************************************************************************
# Configure Defaults
#**************************************************************************************************
set -eu
: ${WPT_BRANCH:='master'}

# Prompt for the configuration options
echo "WebPageTest automatic server install."

# Pre-prompt for the sudo authorization so it doesn't prompt later
sudo date

cd ~
until sudo apt-get update
do
    sleep 1
done
until sudo DEBIAN_FRONTEND=noninteractive apt-get -yq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade
do
    sleep 1
done
until sudo apt-get install -y git screen nginx beanstalkd zip unzip curl \
    php-fpm php-apcu php-sqlite3 php-curl php-gd php-zip php-mbstring php-xml php-redis \
    imagemagick ffmpeg libjpeg-turbo-progs libimage-exiftool-perl \
    software-properties-common psmisc
do
    sleep 1
done
sudo chown -R $USER:$USER /var/www
cd /var/www
until git clone --depth 1 --branch=$WPT_BRANCH https://github.com/WPO-Foundation/webpagetest.git
do
    sleep 1
done
until git clone https://github.com/WPO-Foundation/wptserver-install.git
do
    sleep 1
done

# Configure the OS and software
cat wptserver-install/configs/sysctl.conf | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
mkdir -p /var/www/webpagetest/www/tmp
cat wptserver-install/configs/fstab | sudo tee -a /etc/fstab
sudo mount -a
cat wptserver-install/configs/security/limits.conf | sudo tee -a /etc/security/limits.conf
cat wptserver-install/configs/default/beanstalkd | sudo tee /etc/default/beanstalkd
sudo service beanstalkd restart

#php
PHPVER=$(find /etc/php/8.* /etc/php/7.* -maxdepth 0 -type d | head -n 1 | tr -d -c 0-9\.)
cat wptserver-install/configs/php/php.ini | sudo tee /etc/php/$PHPVER/fpm/php.ini
cat wptserver-install/configs/php/pool.www.conf | sed "s/%USER%/$USER/" | sudo tee /etc/php/$PHPVER/fpm/pool.d/www.conf
sudo service php$PHPVER-fpm restart

#nginx
cat wptserver-install/configs/nginx/fastcgi.conf | sudo tee /etc/nginx/fastcgi.conf
cat wptserver-install/configs/nginx/fastcgi_params | sudo tee /etc/nginx/fastcgi_params
cat wptserver-install/configs/nginx/nginx.conf | sed "s/%USER%/$USER/" | sudo tee /etc/nginx/nginx.conf
cat wptserver-install/configs/nginx/sites.default | sudo tee /etc/nginx/sites-available/default
sudo service nginx restart

# WebPageTest Settings
LOCATIONKEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
SERVERKEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
SERVERSECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
APIKEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
cat wptserver-install/webpagetest/settings.ini | sed "s/%LOCATIONKEY%/$LOCATIONKEY/" | tee /var/www/webpagetest/www/settings/settings.ini
cat wptserver-install/webpagetest/keys.ini | sed "s/%SERVERSECRET%/$SERVERSECRET/" | sed "s/%SERVERKEY%/$SERVERKEY/" | sed "s/%APIKEY%/$APIKEY/" | tee /var/www/webpagetest/www/settings/keys.ini
cat wptserver-install/webpagetest/locations.ini | tee /var/www/webpagetest/www/settings/locations.ini
cp /var/www/webpagetest/www/settings/connectivity.ini.sample /var/www/webpagetest/www/settings/connectivity.ini

# Crontab to tickle the WebPageTest cron jobs every 5 minutes
CRON_ENTRY="*/5 * * * * curl --silent http://127.0.0.1/work/getwork.php"
( crontab -l | grep -v -F "$CRON_ENTRY" ; echo "$CRON_ENTRY" ) | crontab -

clear
echo 'Setup is complete. System reboot is recommended.'
echo 'The locations need to be configured manually in /var/www/webpagetest/www/settings/locations.ini'
echo 'The settings can be tweaked in /var/www/webpagetest/www/settings/settings.ini'
printf "\n"
echo "The location key to use when configuring agents is: $LOCATIONKEY"
echo "An API key to use for automated testing is: $APIKEY"

After installation is successful, you are prompted to reboot.

After rebooting, navigating to the IP address of the virtual machine should show you the WebPageTest interface. Navigating to /install will show you a checklist of dependencies and prerequisites which were configured during the installation.

I also recommend setting display_errors = on in php.ini to see any PHP errors in the browser.

WPT Agent

The WPT Agent is the location from where the tests are run. You may have multiple agents for a single WebPageTest server. This may be useful if you want to test from multiple locations. In this tutorial, you are going to set up a single location with Chrome and Firefox on the same virtual machine as the wptserver. If the WPT agent is hosted on a different machine, you just need to update the configurations.

You can run the following script which sets up the configurations before calling the wptagent-install script. The WPT_KEY must be taken from the WPT server, /var/www/webpagetest/www/settings/settings.ini.

#! /bin/bash

WPT_SERVER="127.0.0.1" \
WPT_LOCATION="localhost" \
WPT_KEY="{WPT_KEY}" \
DISABLE_IPV6=y \
WPT_BRAVE='n' \
WPT_EPIPHANY='n' \
WPT_EDGE='n' \
bash <(curl -s https://raw.githubusercontent.com/WPO-Foundation/wptagent-install/master/debian.sh)

After the agent has finished installing, you will be prompted to reboot.

After rebooting, the OS will execute the script ~/agent.sh. This script initiates the WPT agent. If you do not use HTTPS on 127.0.0.1, you may need to update the agent.sh script to remove https as per below.

python3 wptagent.py -vvvv --server "http://127.0.0.1/work/" --location localhost --key {WPT_KEY} --exit 60 --alive /tmp/wptagent

You can confirm that the agent is running by accessing the nginx logs:

$ tail -f /var/logs/nginx/access.log

127.0.0.1 - - [02/Mar/2023:12:16:26 +0000] "GET /work/getwork.php?f=json&shards=1&reboot=1&servers=1&testinfo=1&location=localhost&pc=webpagetest-192.168.122.253&key={WPT_KEY}&version=230215.171357&dns=127.0.0.53&freedisk=6.641&upminutes=26 HTTP/1.1" 200 5 "-" "wptagent"
127.0.0.1 - - [02/Mar/2023:12:16:31 +0000] "GET /work/getwork.php?f=json&shards=1&reboot=1&servers=1&testinfo=1&location=localhost&pc=webpagetest-192.168.122.253&key={WPT_KEY}&version=230215.171357&dns=127.0.0.53&freedisk=6.641&upminutes=26 HTTP/1.1" 200 5 "-" "wptagent"
127.0.0.1 - - [02/Mar/2023:12:16:36 +0000] "GET /work/getwork.php?f=json&shards=1&reboot=1&servers=1&testinfo=1&location=localhost&pc=webpagetest-192.168.122.253&key={WPT_KEY}&version=230215.171357&dns=127.0.0.53&freedisk=6.641&upminutes=26 HTTP/1.1" 200 5 "-" "wptagent"

This access log shows that the WPT Agent is pinging the WPT Server. If there isn’t any traffic you may want to check if the --server URL in the wptagent.py script is correct. If it returns a 500, try opening the URL in the browser and see if it shows any PHP errors.

Locations

Finally, you need to configure the WPT Server to accept traffic from the WPT Agent you have just configured. This is done in /var/www/webpagetest/www/settings/locations.ini. For the above agent configurations, you can copy-paste the following:

[locations]
1=Local
default=Local

[Local]
1=localhost
label=Ubuntu 22.04
group=Virtual Machines

[localhost]
browser=Chrome,Firefox
label="Ubuntu 22.04"

Now that everything is set up, you should be able to confirm that the installation is correct by navigating to /install. Your agent should show up at the end of the checklist.

WebPageTest 21.07 Installation Check

Limits

Before using your new WPT instance, you might want to revisit the test limits, which default to a maximum of 50 monthly tests. These are configured in /www/settings/settings.ini.

rate_limit_anon_monthly=0
rate_limit_anon=0

You can also set limits on the Web UI or API through the /www/settings/keys.ini file.

Automatic updates

Personally, I recommend disabling automatic updates by commenting out the following line in /www/settings/settings.ini:

gitUpdate=1

Simple Configurations

To create simple configuration presets which show up on the home page, you need to create a /www/settings/profiles.ini file. This file will include a list of presets, including their connection, device, and location.

[MotoG4]
label="<strong><b>MotoG4</b></strong> <img src='/assets/images/test_icons/chrome.svg' alt='chrome'> <span class='test_presets_tag'><img src='/assets/images/test_icons/signal.svg' alt=''>4G</span> <span class='test_presets_tag'>Ubuntu 22.04</span>"
description="Ubuntu 22.04, Simulated MotoG4, 4G Connection, 9 Mbps, 170ms RTT"
location="localhost:Chrome;MotoG4.4G"
runs=3
video=1
timeline=1
fvonly=1
lighthouseTrace=1

In the configuration above, I am creating a configuration named MotoG4 which uses the localhost location with the Chrome browser and emulated MotoG4 with a 4G connection. The label and description affect how the configuration appears on the page, while the runs, video, timeline, fvonly, and lighthouseTrace set some default values.

Screenshot of WebPageTest homepage

In the screenshot above, you can see three simple configurations—iPhoneX, MotoG4, and Desktop—that were added to profiles.ini.

Happy testing!

]]>
Web performance
<![CDATA[First Important Paint - Developing a custom metric]]> https://imkev.dev/custom-metrics https://imkev.dev/custom-metrics Sat, 10 Jun 2023 12:00:00 GMT Updated: Monday Nov 27 2023 | This article is also available in Japanese.

In May 2020, Google proposed a set of user-centric metrics that serve to describe a website’s performance. These are known as the Core Web Vitals (CWV) and include:

The CWVs aim to simplify the many performance metrics available and allow you to prioritize the metrics that matter the most. This abstraction has worked very well. In the years following the CWV announcement, website owners and service providers have invested in web performance, improving the user experience for users all across the web.

A line chart showing a blue line progressing upwards from 20% in Jan 2020 to 40% in Mar 2023

As shown in the chart above extracted from cwvtech.report, the number of websites passing the CWV thresholds has increased from 22% to 40%.

The role of custom metrics

Custom metrics allow you to extend the set of measures already available in the browser with your own measures that might describe your website’s user experience better than the default metrics. For example, LCP assumes that the largest element is also the most important one. This may be true for some websites but not for others.

Filmstrip showing the progress of a page. The third frame is marked as the LCP and only contains a background image. In the 4th frame the title is rendered.

In the filmstrip above, the LCP element is the blue gradient background image as highlighted in the frame with the red border. While arguably, the most important element is the page title “Lorem” which is rendered in the next frame. This might be an exaggerated example, but the same problem exists if the important element is a grid or table, or if the background is a <video> element with a poster image.

Element Timing API

Similarly to LCP, the Element Timing API allows you to measure the time a DOM element is rendered on the page. However, unlike LCP, Element Timing allows the developer to choose which element (or elements) to measure. Element Timing is configured by adding the elementtiming attribute to an element.

One limitation of Element Timing is that it is only supported in Chromium-based browsers. Additionally, it is only supported on a limited subset of elements.

User Timing API

The User Timing API is an API that allows you to set marks and measures at specific points in time. For example, this could be once an element is painted onto the page or once an important task has been executed.

// record the time before the task
performance.mark('doSomething:start');
await doSomething();

// record the time after the task has completed
performance.mark('doSomething:end');

// measure the difference between :start and :end
performance.measure('doSomething', 'doSomething:start', 'doSomething:end');

Real user metrics and lab tests

When developing a custom metric, the first question to ask is whether you want to measure this metric from real users on real devices or if it will only be collected from lab tests. Both types of measures can provide you with value, albeit slightly differently.

Real user metrics (RUM) give you a full spectrum of the user experience. Instead of a single score, you will have a range of values that reflect the different users and experiences. For example, users at the 90th percentile may have a terrible experience while the median user has a good one. Understanding the different factors that influence your metrics—for example, geographical location, browser, or device—will allow you to better optimize your website for your users. On the downside, implementing a RUM solution may be expensive or complex.

Distribution of First Important Paint values following a logarithmic normal distribution ranging from 0s to 7s

The screenshot above is taken from SpeedCurve and shows a distribution of different user experiences.

Lab tests run in a controlled environment. You might be able to configure several parameters, such as connection speed, device, and location from where the tests would run. The test result will be a report detailing the different metrics collected from your page.

Screenshot from WebPageTest's detail view

Lab tests are highly effective at debugging issues. Running lab tests at regular intervals allows you to compare your page’s performance over time and identify regression. My favorite tool for running lab tests is the amazing WebPageTest.

Properties of a good metric

When presenting LCP in 2020, Paul Irish described a list of 8 properties that a good metric should observe—there is also a slight variation documented in the Chromium source code:

  • Representative: Correlates with user experience and business value.
  • Interpretable: The metric and its value are easy to understand.
  • Accurate: How closely the metric measures what it is expected to measure.
  • Simple: The method it is computed is not complex.
  • Orthogonal: It should not be measuring something that is already being measured by other metrics.
  • Stable: There shouldn’t be a lot of variance between runs.
  • Elastic: A small change in the input results in a small change in the metric.
  • Correlates: The metric should correlate well with other metrics and between lab/field tests.

Observer Effect

Another important property of a good metric is that it does not influence the test itself. In science, the Observer effect is described as the disturbance of a test by the act of observation. If measuring a custom metric measurably slows down the page, then the metric is not a good one.

First Important Paint

First Important Paint (FIP) is a custom metric that was developed to measure the time it takes to paint the first annotated HTML element onto the page.

The need for this metric arose because the LCP element did not always correspond to the user experience. Additionally, pages were composed of different components—often built by developers on different teams. By annotating the element—instead of the page—the developers creating the component do not need to know on which page the component is being used as it would automatically be treated as an FIP candidate.

(It also comes with a three-letter acronym so you know it’s legit 😛)

Is FIP a good metric?

First Important Paint can be measured both in the field and in the lab. To determine if FIP is a good metric, I ran some lab tests and collected data from two production websites in unrelated markets.

Representative

To test if FIP is representative, I prepared some lab tests with screenshots from different pages and devices and asked colleagues with no knowledge of the metric to tell me in which frame they would consider the most important element to be painted. We used this data to decide which components should be treated as important and to assess whether LCP was already representing this information.

Interpretable

The First Important Paint is easy to understand and is measured in milliseconds.

Accurate

To measure the accuracy of the custom metric, I ran some lab tests where the FIP element was also the LCP element. This means that FIP should record a similar value as LCP. For text elements, I used the Element Timing API instead of LCP.

The tests were executed for image and text elements using 3 different connection speeds. For native connections, the FIP differed from the LCP by 50ms. For Regular 4G and Regular 3G connections, the difference between FIP and LCP elements increased to 200ms and 1000ms respectively.

While the results may appear underwhelming the measurements were consistent and the difference between FIP and LCP or Element Timing did not vary greatly between one test and another. For example, it would have been more concerning if one test registered an FIP that is 50ms faster than LCP and the successive test registers an FIP that is 50ms slower than LCP.

The reasons for the inaccuracy are various and are limitations in how the metric is computed.

Simple

The code for measuring and computing FIP is simple and can easily be debugged or modified if needed.

Orthogonal

FIP is a custom metric that aims to measure the most important element on the page. In some cases, this might be the same as the largest element, in which case it would overlap with LCP.

Stable

To measure the stability of First Important Paint, I ran lab tests on 3 different connection speeds.

Line chart showing three lines. At the bottom near 0ms, a blue line for Native Connection with small variations. Slightly above it at 1000ms red line with no variation for Regular 4G. Over 4500ms, a yellow line for Regular 3G.

As shown in the chart above, all connections experienced very little variation between one test and another.

Elastic

A metric is elastic if it responds proportionately to a change in the user experience. To test if FIP is elastic, I created a demo to serve as a baseline and a variation of this page that self-hosts and preloads the font files. It is expected that the user experience should improve as the title is rendered earlier—and as a result, FIP should also improve.

One of the earlier iterations of FIP did not record any improvements after this change. This helped me identify a flaw in the metric as FIP was recording the time a text element was added to the DOM but did not consider cases when the web fonts have not yet been downloaded.

After this issue was resolved we can see a small and proportional improvement in FIP that matches the improved user experience.

Filmstrip from WebPageTest comparing two pages. The first test, labeled Original, loads the background image at 1.3s and the title at 1.5s. The second test labeled Optimized, loads the title at 1.1s and the background image remains at 1.3s.

In the comparison above, the Original test reported an LCP of 1.315s and FIP of 1.470s. The Optimized test—which included the fixes above—reported an LCP of 1.305s and an improved FIP of 1.188s. This coincides with the user experience, where the second test has a much-improved experience.

Correlates

To check if FIP correlates with other metrics, I used the same tests described earlier in Accuracy. I plotted FIP against LCP in a scatter plot and calculated the coefficient of determination. For the two metrics to correlate, they do not need to have the same value, but an increase in LCP should result in a proportional increase in FIP and vice-versa.

Scatter plot showing a First Important Paint on the horizontal axis and Largest Contentful Paint on the vertical axis with a positive correlation.

For text elements, Regular 4G recorded a strong correlation while Regular 3G did not register an equally strong correlation.

Using real user data available in SpeedCurve we can also check if FIP correlates to business metrics.

A correlation chart showing a blue line representing Bounce Rate increasing as FIP increases.

The chart above is extracted from SpeedCurve and shows a positive correlation between the page’s bounce rate (blue line) and FIP—as FIP increases so does the bounce rate.

Observer Effect

Finally, we want to confirm that the metric does not influence the performance of the page being tested. We ran some tests that contained the measurement and the same tests that do not use the measurement—but include a similarly sized render-blocking script.

A line chart showing blue and red dots placed in line with each other representing the set of tests executed with and without FIP.

As shown in the chart above, the LCP element did not vary between the two sets of tests. In the future, we may also want to extend this test to include JavaScript long tasks in addition to LCP.

Conclusion

The goal of this document is not to promote FIP, but to initiate an internal process of metric reviews. Understanding the scope of the Core Web Vitals and what makes a good custom metric allows us to be critical of our metrics and improve them.

So, is First Important Paint a good metric? It has its limitations and your mileage may vary, but for us, it currently serves an important purpose. I do hope we can improve it and add new custom metrics that describe other aspects of the user experience too.

Thank you for reading. I’m always eager to learn, so feedback and questions are welcome.

]]>
Web performance Web development
<![CDATA[Web Performance Audit]]> https://imkev.dev/performance-audit https://imkev.dev/performance-audit Wed, 07 Jun 2023 12:00:00 GMT Updated: Wednesday July 12 2023

I was asked to perform a performance audit for an online casino operating in Europe. My client noticed that their website is slower than their competitors and they knew that it was costing them millions in lost revenue. This article is an overview of the process, analysis, and recommendations that went into that audit.

My first step when starting a performance audit is to understand the current situation and set up a baseline. Relying on Lighthouse or lab tests alone might not represent what your users are experiencing so I always recommend looking at real user metrics (RUM). For this client, we installed SpeedCurve. Alternatively, you could use the RUM data already available in CrUX.

We then configured some lab tests on WebPageTest. RUM told me that 88% of users are on a mobile device and 99% are on a 4G connection or better. Therefore we set up the lab tests to run on an emulated iPhone X and a Moto G4—using an LTE and 4G connection respectively. We also ran some tests on desktop devices but our main priority was mobile.

In addition to the home page, we also ran tests against other pages that play a critical role in the user journey. The RUM data for these pages was only available in SpeedCurve as the inner pages did not meet the CrUX eligibility criteria.

Largest Contentful Paint (LCP)

The first thing we noted is that the LCP element on the home page is a placeholder image and not representative of the user experience.

Dark grey background with a light grey loading spinner

If we run the same test but block the above LCP element the LCP increases from the current 1.5s to 4.5s. The new LCP element is the hero image shown on the home page banner. This is much more representative of the user experience and should be the loading experience we should be optimizing for.

To avoid the placeholder image becoming our LCP element we can reduce the size of the image—for example by using CSS for the background color—or reduce the bits-per-pixel to not pass the 0.05 bpp threshold. If either of these is not possible, then it might be worthwhile to add a custom User Timing—for example First Important Paint.

The new LCP image is the 79th requested element. It is a late-discovered resource—such as a CSS background-image or an element added to the DOM using JavaScript. For this client, while the <img> element is present in the initial document, the src attribute is populated using a JavaScript lazy-loading library. Lazy-loading the LCP element is an anti-pattern and can delay rendering.

Cumulative Layout Shift (CLS)

The main contributor to CLS is a UI issue in the slider which appears on the home page.

Filmstrip showing the loading of the hero image

As can be seen in the image above, the width of the content of the slider increases as the first slide transitions into view—starting from 0px until it reaches the full viewport width.

To test my hypothesis, we added width: 100% !important to the .slider-slide CSS selector using WebPageTest experiments, forcing the slider’s contents to always take up the full viewport width rather than transition from 0px. This eliminated the layout shift from the home page banner content resulting in an improvement from 1.20 to 0.17.

The second largest contributor to the CLS score is a layout shift of the footer.

Screenshot of homepage showing the footer within the initial viewport

Until the game thumbnails load, the footer is shown within the initial viewport before being pushed downwards and outside of the viewport once the game thumbnails have loaded. If the server-rendered HTML does not include the game data, it may be sensible to reserve the space for the game thumbnails or show skeleton placeholders. In this client’s case, the initial HTML already included the games but they were being removed because of a bug in the hydration phase.

Time to First Byte (TTFB)

The lab tests for the home page are showing a TTFB of 352ms, while the lab tests for the game page are showing a TTFB of 661ms. These are both much less than the 2.1s recorded by real users on both CrUX and SpeedCurve.

Grouped horizontal bar chart showing three test runs

On further investigation and as visible in the chart above, the TTFB of the second run has a terrible 8.9s TTFB—while the first and third tests have a TTFB of approximately 660ms. After a few more tests we were able to confirm that it was not an anomaly and a high TTFB was recorded almost once every four page views. We loaded the page in the browser a few times to check if this only occurs on WebPageTest but sure enough, we experienced a high TTFB within a few refreshes.

I advised the development team to set up monitoring on the server and include server-timing response headers to allow us to correlate the high TTFB with a server process. Based on the latest update I have received from the development team, the high TTFB was caused by cache invalidation on node-cache and will be re-implemented to use a “stale-while-revalidate” strategy.

Document Object Model (DOM)

The home page has a large number of DOM nodes which can affect load time and interactivity.

document.querySelectorAll('*').length; // 3704

After the page has loaded it has a total of 3,704 DOM nodes. It is possible to reduce the number of DOM nodes by flattening a tree structure.

<div class="Description">
  <p>
    <span style="font-size: 20px;">
      <strong
        >The
        <span style="color: #ffe4b5;">Lorem Ipsum</span>
      </strong>
    </span>
  </p>
</div>

If we look at the above HTML structure we are creating 5 DOM nodes to display a short string. This can be refactored to only require two elements without changing its appearance:

<p class="Description" style="font-size:20px; font-weight: bold">
  The <span style="color: #ffe4b5;">Lorem Ipsum</span>
</p>

When deciding which HTML to refactor, you should search for HTML that appears on the page many times. This way any savings will be multiplied by the number of times that the HTML appears on the page.

A good example on this page is the PlayButton <span> element that includes an <svg> element. The same HTML is repeated on each game thumbnail—a total of 238 times. This can be refactored to use an external image in an <img> element or a CSS background-image. Applying a long cache lifetime means that the image will not be downloaded more than once while also improving the user experience for repeat visitors.

JavaScript

The page downloads a lot of JavaScript—a large part of which remains unused at page load. Code-splitting is a common technique to delay the loading of non-critical JavaScript.

The website is developed using React so it is possible to implement code-splitting using React.lazy.

The page also downloads a JavaScript file /static/js/svg-0.js which seems to contain embedded SVGs. JavaScript requires more CPU processing to parse than SVG or HTML, so it may be better to avoid embedding SVGs in JavaScript and instead reference the SVG files as external resources. This client was using webpack so it only required a small alteration to change the loader in the webpack config from svg-url-loader or url-loader to file-loader. Jacob Grob has written a detailed article on this topic.

The page also has a single render-blocking script https://cdn.trackjs.com/agent/v3/latest/t.js that is a third-party resource. Third-party blocking requests are risky as your page’s performance is dependent on the third party’s response time. Can this script be deferred using the defer attribute?

Images

The website uses a JavaScript library for image lazy-loading. We can replace this with the browser’s native image lazy-loading which is available in all major browsers. Additionally, all images are being lazy-loaded, including those in the initial viewport. Only use loading="lazy" for images outside the initial viewport. If you are unsure whether an image would appear in the viewport or not, I recommend that you are cautious and do not lazy-load these images. The browser usually does a good job with resource prioritization.

The page has a large number of images in different formats. WebP is supported on all major browsers and should be the preferred option with a fallback for older formats. The page also includes GIF images that are highly inefficient and should not be used.

The page also features a large number of SVG animations. This is causing two issues. The first issue is that each SVG image includes several Base64 images encoded within the SVG. This results in image files that are larger than their JPEG or WebP equivalent. Additionally, these images cannot be compressed since Base64 is not easily compressed.

Secondly, SVG animations do not run on the compositor thread unless the device has a DPR of 1.0 and the SVG is inlined. This page includes all SVG animations using the <img> tag which causes CPU (and GPU) usage to increase significantly.

Screenshot from Chrome's performance panel showing frequent spikes in layout and paint

Inlining the animations would improve performance on some devices—with a DPR of 1.0—however, most users would not benefit from this and embedding all the animations could significantly bloat the HTML. I would recommend limiting the number of animated SVGs on each page and using CSS transforms when possible.

Finally, we can improve the cacheability of the banner images by increasing the max-age to 31536000 (1 year) since each new banner image gets its unique URL and fingerprint. The same could be done for the favicon and android-icon that currently have a max-age of 200.

Fonts

The three font files are served in the modern WOFF2 format and already use font-subsetting to reduce their size. This is great news as the average font file is only around 15KB. However, they are only cached for 200 seconds. The max-age for all three font files can be increased to 31536000—1 year.

Browser Hints

Fetch Priority API

The Fetch Priority API is supported on Chrome and will soon be enabled on Safari. You can add fetchpriority="high" to prioritize critical assets. Consider adding fetchpriority="high" on the first banner image (the LCP element) and the <script> files.

Preload

The page makes use of some <link rel="preload"> elements to instruct the browser to fetch JavaScript and CSS files. Considering that these files are located in the document <head> and are not late-discovered resources, we can remove the preload for CSS and script files.

Some <script> tags are located before the closing <body> tag. These could be moved to the <head> and the defer attribute added to each of them to make sure that they are not render-blocking.

Preconnect

The page is dependent on the storage.googleapis.com and static.everymatrix.com domains for showing critical content—including the LCP image. Preconnecting to these domains using a <link rel="preconnect"> element can reduce the time it takes to download these resources by opening a connection earlier.

<title>

In most cases, the <title> should be placed as high as possible within the document’s <head> as this gives the user immediate feedback on the content of the page. The <title> element should be placed beneath any <meta charset|http-equiv|viewport> elements. This won’t affect any metrics but it is a good practice to follow.

Legacy code

The page contains some HTML elements specific to Internet Explorer that can be safely removed. This includes an http-equiv="X-UA-Compatible" meta element and a few conditional comments—such as <!--[if lt IE 7]>.

The page also uses an old pattern where the CSS class no-js is added to the <html> element to recognize if the page is interactive—the no-js class is usually removed as soon as JavaScript is executed. However, adding and removing CSS classes to the <html> element causes a Recalculate Style which can slow down the page. This can be safely removed.

Server-side rendering

It is unclear what the intended role of the server-rendered content on the home page is.

Splash screen for the homepage

The content is hidden behind a splash screen containing the brand logo. By removing the splash screen using another WebPageTest experiment we were able to show the page content much earlier.

Screenshot of homepage with JavaScript disabled

If we can fix the URLs and consistently render the page content it could result in a huge improvement. As part of the deliverables I sent to the client, I included a video comparing the experiment that hides the splash screen using CSS display: none with the current loading experience. The experiment is a full second faster to show the game grid and meaningful content.

Going forward

The above document resulted in 21 issues/tasks that were presented to the client. I am in regular contact with the client in case a ticket is not clear or requires further clarification and we have set a three-month timeline to implement most of my recommendations.

Thank you for reading. Please get in touch if you have any questions or feedback.


Disclaimer: The wording of the original document has been modified for a public audience. Any identifiable information, including images, has been modified or removed to protect the client’s privacy. The audit was conducted in May 2023.

]]>
Web performance
<![CDATA[Fetch Priority and optimizing LCP]]> https://imkev.dev/fetchpriority-opportunity https://imkev.dev/fetchpriority-opportunity Mon, 02 Jan 2023 12:00:00 GMT Updated: Tuesday May 02 2023 | This article is also available in Japanese.

The Fetch Priority API is used to indicate to the browser the relative priority of a resource. You can configure the priority by adding the fetchpriority attribute to <img>, <link>, <script>, and <iframe> elements or through the priority attribute on the Fetch API.

The browser’s loading process is complex. Browsers determine a request’s priority mostly by its type and its position in the document’s markup. For example, a CSS file requested in the document’s <head> will be assigned the Highest priority, while a <script> element with the defer attribute will be assigned the Low priority. The browser downloads resources with the same priority in the order in which they are discovered.

fetchpriority

The fetchpriority attribute can be used to hint the browser to increase or decrease the priority of a requested resource. The enumerated attribute can have one of three values:

  • high - The resource is more important relative to its default priority
  • low - The resource is less important relative to its default priority
  • auto - The default value
<img src="/lcp.jpg" alt="A dog" fetchpriority="high" />

In the example above, we are hinting to the browser that the <img> priority is more important than its default priority.

The same values are supported for the priority attribute on the fetch method.

fetch("/api/data.json", { priority: 'high' })

In the fetch request above, we are indicating to the browser that the fetch request has an increased priority compared to its default priority.

Default priority

The Fetch Priority API increases or decreases a resource’s priority relative to its default priority. For example, images - by default - always start at a Low priority. Assigning fetchpriority="high" will increase their priority to High. On the other hand, a render-blocking stylesheet is assigned a Highest priority by default. Assigning it fetchpriority="low" will lower its priority to High - but not Low. fetchpriority is used to adjust a resource’s priority relative to its default, rather than to explicitly set its value.

The influence of Fetch Priority on resource prioritization in Chromium documents the different resource types, their default priority (◉), and the resultant priority when using fetchpriority="high" (⬆) and fetchpriority="low" ().

Note that if an image is discovered to be within the viewport, then its priority is boosted to High. However, this could be quite late in the loading process and may have little or no impact if the request was already sent. Using fetchpriority="high" allows you to tell the browser to start in High priority, rather than waiting for the browser to find out if it is in the viewport or not.

“Tight mode”

Most browsers download resources in two phases. During the initial phase (Chromium also refers to this as “Tight mode”), the browser does not download Low priority resources unless there are less than two in-flight requests.

WebPageTest waterfall chart illustrating the initial phase

In the waterfall chart above, you could see that the resource image-1.jpg does not start downloading until style-2.css has finished downloading - even if it was discovered immediately. At this point, only one resource remains in-flight - script.js, so the browser begins to download the Low priority image.

The initial phase is completed once all blocking scripts in the <head> have been downloaded and executed (scripts with async or defer are not render-blocking). Even if there are more than two in-flight requests, the browser can now proceed to download any remaining resources based on their priority and the order in which they appear in the markup.

WebPageTest waterfall chart illustrating DOM Interactive

In the chart above, once the render-blocking JavaScript is downloaded and executed (pink bar), the browser begins downloading the images, even if the two CSS files are still in-flight. The yellow vertical bar illustrates DOM Interactive - or when the readystatechange event was fired.

preconnect

If the images reside on a separate domain, the browser needs to open a connection to the domain before downloading the files.

WebPageTest waterfall chart illustrating crossorigin images

This is shown on the WebPageTest chart with the green, orange, and magenta bars preceding the downloading. We can start downloading the images earlier using the preconnect resource hint.

WebPageTest waterfall chart illustrating  resource hint

In the chart above, the connection to the cdn.glitch.global domain is opened during the initial phase - before the browser is able to start downloading the files. Once the browser exits the initial phase (yellow vertical line) it begins downloading the images immediately - saving approximately 350ms.

preload

If we were able to improve the download time using the preconnect resource hint, are we able to improve it further using the preload directive? Short answer: no. The preload directive allows you to inform the browser about critical resources that are “late-discovered”. This is especially useful for resources loaded inside stylesheets or scripts, such as background-images or fonts. In our example, the image is declared in the markup and discovered early, so preload has little effect.

WebPageTest waterfall chart illustrating  directive

In the chart above, we have replaced the preconnect hint with the following:

<link
  rel="preload"
  as="image"
  href="https://cdn.glitch.global/.../image-1.jpg"
/>

Despite the preload, the image still doesn’t begin downloading until there are less than two requests in-flight.

fetchpriority

We can use Fetch Priority to indicate to the browser that image-1.jpg is more important than its default priority using:

<img
  src="https://cdn.glitch.global/.../image-1.jpg"
  fetchpriority="high"
  alt=""
/>

This should increase the initial priority of the image from Low to High, allowing the image to be picked up in the initial phase.

WebPageTest waterfall chart illustrating

The waterfall chart above shows that image-1.jpg is picked up during the initial phase, in parallel with the other critical resources. This gives us the greatest improvement so far.

Firefox

Firefox uses similar heuristics to determine which resources should be loaded during the initial phase. However, differently from Chromium-based browsers, it does not begin downloading any Low priority resources until all JavaScript in the <head> is downloaded and executed - even when there is only one High priority request in-flight.

Screenshot from Firefox Web Developer Tools illustrating the initial phase

The above screenshot is taken from Firefox Web Developer Tools and shows that the image resources (rows 5 - 8) are fetched after the script (row 2) is downloaded and executed and the page becomes interactive - vertical, blue line.

While Chrome waits for JavaScript declared in the <head> to be downloaded and executed, Firefox waits for all render-blocking JavaScript declared before the image elements - even if these are declared outside of the <head>.

Firefox does not support fetchpriority yet, however, we can increase the priority of image-1.jpg using the preload directive.

Screenshot from Firefox Web Developer Tools illustrating the initial phase

In the screenshot above, the file image-1.jpg is fetched in parallel with the other resources. This is similar to the behavior we have seen when adding fetchpriority="high" on Google Chrome.

Safari

Safari on iOS and macOS also has an initial phase although it behaves differently than Chrome and Firefox.

Low priority resources start being fetched when there are fewer than two in-flight requests. It is not dependent on the readystatechange event and even on pages without any render-blocking JavaScript, the browser will wait until there is one in-flight request.

Screenshot showing Safari Web Inspector tight mode

In the screenshot above, taken from Safari’s Web Inspector, the images do not start downloading until style-1.css finishes downloading and there are less than two in-flight requests.

On Safari, the initial phase only applies to resources from the same origin. If the Low priority resources are loaded from a different domain they will be fetched as soon as they are discovered.

Screenshot showing Safari Web Inspector not restricting crossorigin  priority requests

In the screenshot above, the crossorigin images are fetched immediately without waiting for the High priority resources to finish downloading.

The preload directive does not affect the resource’s priority. However, placing the <link rel="preload"> directive before High priority requests will cause it to download earlier; since at the time it is discovered there are less than two requests in-flight. This is the same behavior seen on other browsers and in most cases, I would advise against placing preload directives above High priority resources as render-blocking CSS should take precedence.

Screenshot of Safari Web Inspector illustrating the  directive

In this screenshot, the Low priority file image-1.jpg begins downloading before the High priority style-1.css file because the <link rel="preload"> is placed above it in the document markup.

Combining preload with fetchpriority

Fetch Priority is only supported on Chromium-based browsers so far, however, it fails gracefully on unsupported browsers that do not recognize the fetchpriority attribute. This allows us to combine the preload directive with Fetch Priority.

<link
  rel="preload"
  as="image"
  fetchpriority="high"
  href="https://cdn.glitch.global/.../image-1.jpg"
/>

Browsers that support Fetch Priority will preload the resource using the assigned fetchpriority, while browsers that do not will use the preload directive.

WebPageTest waterfall chart showing  and

The above chart shows similar results to the one earlier which included the fetchpriority attribute on the <img> element. The advantage of this method is unifying an approach that prioritizes the resource on browsers that support Fetch Priority and on those that do not.

fetchpriority all the things

In this section, we will look at the potential benefit of using fetchpriority. All data is taken from the HTTP Archive and we are only considering pages that use HTTP/2 or HTTP/3 and where the Largest Contentful Paint (LCP) element is an image. All queries and results are publicly available.

Note: The HTTP Archive data is collected using a private instance of WebPageTest using Chrome. You can learn more about their methodology.

WebPageTest waterfall chart illustrating the opportunity for the LCP image with a horizontal red line

I am assuming the benefit from fetchpriority as the difference between the time the resource is discovered and the time it starts downloading. I refer to this as the opportunity. Therefore if a resource is discovered early but the browser starts downloading it late, then the opportunity is greater.

Note that if the images are served from a different domain, I am including the time to open the connection in the opportunity.

Combination chart showing opportunity vs LCP

The chart above plots the opportunity (in milliseconds) against the LCP. The opportunity is bucketed in groups of 100ms, while anything greater than 1,000ms is grouped into a single bucket. The chart shows a strong correlation between the opportunity and the LCP - the greater the opportunity, the worse the LCP.

Bar chart showing distribution of opportunity by initial priority

The above chart shows the distribution of the opportunity for mobile devices for Low and High priority. At the median, an LCP image requested with High priority starts to be downloaded 21ms after it is discovered, while an LCP image with Low priority is downloaded after 102ms. The difference grows even further at the 75th and 90th percentile.

In addition to fetchpriority="High", an image may have an initial High priority if the image is late-discovered, for example when using CSS background-image or adding an image using JavaScript. In these cases, fetchpriority would not help since the request already has a High priority.

We can conclude that there is a clear benefit in prioritizing your LCP image. The opportunity varies depending on your page’s composition. We have already covered that Low priority resources are not fetched immediately when there is at least one render-blocking script and two or more in-flight requests.

Combination chart showing the number of render-blocking resources vs median opportunity

The above chart plots the number of render-blocking resources against the opportunity (in milliseconds). Intuitively, the more render-blocking resources your page has, the greater the delay in downloading the LCP image.

Conclusion

There is a big opportunity available to prioritize your LCP image through Resource Hints and Fetch Priority. Many pages have the LCP element queued and waiting, even when it is immediately discoverable in the main document.

Distribution of opportunity
The above chart shows that on the median mobile website, the LCP image is queued for 98ms until the browser starts downloading it. At the 90th percentile, the LCP image is queued for 810ms. Using Fetch Priority could increase the priority of the LCP image and reduce this waiting time.

There are also case studies showing an improvement to Largest Contentful Paint (LCP) after adding fetchpriority="high" to the LCP image. Etsy saw a 4% improvement, some others reportedly saw 20-30% improvements.

Increasing the priority of a resource usually comes at the cost of another resource, so Fetch Priority should be used sparingly. However, if the browser is queuing your LCP image, I recommend you experiment with Fetch Priority to see if you can reduce this waiting time and improve your LCP.

In a nutshell,

  • Host your LCP image on the same domain as your HTML document. If this is not possible, use preconnect to open an early connection.
  • The LCP image should be part of the document markup. If you are unable to do this, use preload to tell the browser to download the image before it is requested.
  • Avoid blocking resources when possible. If your LCP image is downloaded with a Low priority, use fetchpriority to hint the browser to download your image earlier.
  • You can use preload to prioritize your LCP image on Firefox until fetchpriority is supported. Safari does not download images earlier when using the preload directive.

Let me know what you think. Your feedback is welcome. ♥

Special thanks

Special thanks to Barry Pollard for his advice and valuable feedback.

Notes

  • This feature was originally called Priority Hints but was renamed to Fetch Priority after standardization.
]]>
Web performance Core Web Vitals Web development
<![CDATA[The 2022 Web Almanac - JavaScript, Third Parties and Interop 2022]]> https://imkev.dev/2022-web-almanac https://imkev.dev/2022-web-almanac Tue, 27 Sep 2022 12:00:00 GMT For the second year running, I have contributed to the latest edition of the HTTP Archive’s Web Almanac. This year I did not author any chapters, but I was involved as an analyst and reviewer on three chapters:

The Web Almanac brings together many experts from different fields and it is a humbling and incredible learning experience to collaborate on this initiative. While working on these chapters, we uncovered data that we did not expect or that was previously undocumented, confirmed initial hypotheses, and learned more about the current state of the web.

Interoperability

Interoperability is a new addition to the Web Almanac and complements the Interop 2022 initiative that aims to bring the major browser vendors together to resolve compatibility issues and improve the developer experience in some key areas.

One of these key areas is cascade layers a new feature recently added to CSS. Cascade layers allow authors to avoid specificity conflicts that arise in CSS, especially when working with third parties. Using the @layer at-rule, we can establish our layers and specificity will only apply within each layer.

@layer site {
  .my-single_class {
    color: rebeccapurple;
  }
}

As this feature is still very new, only 0.003% of pages in the dataset contained an @layer ruleset, but hopefully, the work of the Interop 2022 initiative pays off and we see greater adoption soon.

JavaScript

The amount of JavaScript being downloaded on mobile devices has increased by 8% year on year, making it clear that we need to pay attention to both the quantity and the quality of the JavaScript we are shipping.

Bar chart showing the percentage of pages that use Babel, in decreasing order of popularity. On mobile pages, the values are 40% of the top 1k, 40% of the top 10k, 32% for the top 100k, 23% of the top 1M, and 26% over all websites. Desktop pages trend close to mobile.

Pages using Babel grouped by rank

For the first time, we are parsing source maps to better understand how the JavaScript we are shipping is built, with webpack bundling 17% of the top 1,000 sites and Babel transpiling 40% of the top 1,000 websites and 26% of the entire dataset. With such widespread adoption, new features and improvements to these tools would have a significant impact on the JavaScript we consume.

In addition to how we are compiling and serving JavaScript, the JavaScript we write could also require some attention, with 67% of pages shipping legacy JavaScript, 18% using the unfavorable document.write, and 2.5% still using synchronous XHR. While newer and more performant methods, such as the Scheduler API only see 0.002% adoption so far.

As the web platform matures, we hope to see increased adoption of these modern APIs and an improved and faster user experience.

Third Parties

Bar chart showing the percentage of mobile pages that have main thread blocked by a third party by top 10 third parties. YouTube is blocking the main thread on 90% of mobile pages, Google Maps on 85%, Other Google APIs/SDKs on 84%, Facebook 82%, Google Dounbleclick Ads 81%, Google CDN 79%, Google Tag Manager 75%, Cloudfare CDN 71%, Google Analytics 70%, Google Fonts 63%.

Third parties blocking the main thread

Looking at the performance impact of third parties, YouTube blocks the main thread on 90% of mobile websites tested, with a median main thread blocking time of 1,721ms. On the other hand, third parties do a good job at minifying and compressing resources, with 88.4% of scripts and 95.9% of CSS compressed with either GZip or Brotli.

All data and queries are publicly available and feedback is highly encouraged.

Thank you and have a good one!

]]>
Authoring Web Almanac JavaScript Interop Third Parties
<![CDATA[Google IO Extended - Malta]]> https://imkev.dev/google-io-extended-malta https://imkev.dev/google-io-extended-malta Wed, 07 Sep 2022 12:00:00 GMT On June 21st I was lucky enough to speak at Google IO Extended - Malta. This was my first in-person meetup since COVID, so it was great seeing some familiar faces and even meeting a bunch of new people.

I enjoy sharing and speaking about my work. Teaching is one of my favorite parts of the job. If you would like me to speak at your event, conference, or local meetup please get in touch.

I am also available to conduct company workshops or presentations. The content will be made specifically for you and your team.

* The event wasn’t recorded, so I recorded a remote session from home.

]]>
Web performance Speaking
<![CDATA[Interaction to Next Paint]]> https://imkev.dev/inp https://imkev.dev/inp Thu, 16 Jun 2022 12:00:00 GMT Updated: Friday Feb 2 2024

In May 2022, Google added Time to First Byte (TTFB) and Interaction to Next Paint (INP) to their CrUX report. INP measures a website’s responsiveness and will replace First Input Delay (FID) as a Core Web Vital on March 12th, 2024.

Let’s take a look at what Interaction to Next Paint is and how can you prepare your website to have a good INP.

First Input Delay

Before looking at INP, let’s review First Input Delay, the Core Web Vital currently used to measure responsiveness to user interaction. FID measures the delay it takes the browser to begin processing the first interaction on a page.

In single-threaded JavaScript, if the main thread is busy processing a task, any user input is delayed until the call stack is clear and all tasks have been completed.

An example of poor responsiveness. The user key events do not update the screen until a long task has been completed and all characters are added at once.

FID is directly correlated with JavaScript long tasks. The more long tasks on your website, the more likely that the user input will be delayed as the browser must wait for the current long task to complete before processing input.

The percentage of origins with good FID experiences, less than or equal to 100 ms

The number of websites achieving a good FID score has increased from 72.1% of origins in 2018 to 92.5% of origins in May 2022. FID on desktop was always > 98%.

Interaction to Next Paint

INP aims to build on FID but considers the responsiveness of all user inputs. It also captures the entire duration of the interaction, until the next frame is painted onto the screen. Check out this demo to get a feel of how frustrating a high INP could be.

What is an interaction?

An interaction is one of the following:

  • Clicking or tapping on an interactive element, such as a button or checkbox.
  • Pressing a key, including text input fields.

It does not include hover or scroll events.

How is an interaction’s duration measured?

Breakdown of an interaction

The phases of a single interaction. The input delay occurs from the time an input is received and may be caused by factors such as blocking tasks on the main thread. The processing time is the time it takes for the interaction’s event handlers to execute. When execution finishes, we then enter the presentation delay, which is the time it takes to render and paint the next frame. Source: https://web.dev/inp/

An interaction consists of three phases, the input delay, the processing time and the presentation delay. The duration of an interaction as measured by INP is the sum of the time for all three phases. In simpler words, it is the time it takes from when the user interacts with an element or input until the next frame is painted on the screen. This could be any visual cue that the user input has been received, such as displaying the character in a textbox or displaying a loading animation after clicking a button.

What is a good INP?

In most cases, INP measures the worst interaction on your page. For highly interactive pages with more than 50 interactions, then INP will pick the 98th percentile. A good INP is one under 200 milliseconds, while a poor INP is one over 500ms. Anything in between means that your page needs improvement.

Why is INP important?

Even if Google does not add INP as a Core Web Vital, INP is descriptive of your user’s experience on your website. Having a good INP means that your website responds quickly to user input and provides your users with a delightful experience.

Correlation between JS Long Tasks and Conversion Rate

A chart demonstrating a negative correlation between JS Long Tasks and Conversion Rate. The more long tasks, the less likely a user is to convert.

The above chart - taken from RUM data from a client of mine - shows a strong negative correlation between JavaScript long tasks and the conversion rate. The median conversion rate for users that experience < 500ms JS long tasks is 38.5%, while users that experience > 4000ms long tasks have a conversion rate of 11.8%. This has a tremendous business impact and is more strongly correlated than any of the Core Web Vitals!

And with INP 2x more correlated to Total Blocking Time (TBT) than FID, we expect to see a similar negative correlation between INP and the conversion rate.

How do you optimize INP?

As INP is correlated well with TBT, you could imply that reducing TBT will reduce your INP.

  • Minimize main-thread work.
  • Optimize your JavaScript bundles by code-splitting.
  • If you are working on a React app, you should minimize hydration and optimize your rerenders.
  • Audit your third parties to ensure they aren’t bloating your main thread on page load or affecting page responsiveness on user interaction, such as event handlers.

Tooling

The best way to measure INP is to collect metrics from real users. If your website meets the criteria to be included in the CrUX dataset, then you can use the CrUX API’s experimental_interaction_to_next_paint field or via BigQuery.

PageSpeed Insights

INP is now available on PageSpeed Insights.

You may view INP data using PageSpeed Insights or Treo; however, these still rely on the CrUX dataset.

You can measure the INP metric yourself by using the web-vitals library and sending this data to Google Analytics or another analytics endpoint.

let maxDuration = 0;

new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    // Comment this out to show ALL event entry types (useful e.g. on Firefox).
    if (!entry.interactionId) continue;

    if (entry.duration > maxDuration) {
      // New longest Interaction to Next Paint (duration).
      maxDuration = entry.duration;
      console.log(`[INP] duration: ${entry.duration}, type: ${entry.name}`, entry);
    } else {
      // Not the longest Interaction, but uncomment the next line if you still want to see it.
      // console.log(`[Interaction] duration: ${entry.duration}, type: ${entry.name}`, entry);
    }
  }
}).observe({
  type: 'event',
  durationThreshold: 16, // Minimum supported by the spec.
  buffered: true
});

If you’re looking to debug INP on your website, you can create a PerformanceObserver and log the longest interaction. Note that this is a simplification of the INP metric and may vary.

INP in the wild

According to the HTTP Archive, only 56.4% of all mobile origins have a good INP. While 95.7% of desktop origins have a good INP.

Technology Origins Good INP
ALL 6,754,899 55%
jQuery 5,591,806 57%
AngularJS 85,049 46%
Vue.js 211,468 42%
React 582,000 35%
Preact 183,771 35%
Svelte 8,062 33%

CWV technology report - INP data for May 2022

The above table is extracted from the HTTP Archive as of May 2022, using the CWV Technology Report. Most modern front-end frameworks do poorly in INP, which isn’t surprising considering how much heavy lifting is being done in JavaScript.

Thank you for reading. I would love to hear your feedback and please reach out if you would like to see more correlations involving INP.

]]>
Web performance Core Web Vitals