<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Kevin Farrugia</title>
        <link>https://imkev.dev/</link>
        <description>Frontend engineering and web performance, by Kevin Farrugia</description>
        <lastBuildDate>Tue, 15 Apr 2025 11:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>imkev.dev</generator>
        <language>en</language>
        <ttl>60</ttl>
        <image>
            <title>Kevin Farrugia</title>
            <url>https://imkev.dev/media/logo.jpg</url>
            <link>https://imkev.dev/</link>
        </image>
        <copyright>Copyright 2023, Kevin Farrugia</copyright>
        <category>Web Performance</category>
        <category>Frontend Engineering</category>
        <category>Technology</category>
        <category>Web Development</category>
        <atom:link href="https://imkev.dev/feed" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Adding a CrUX Vis shortcut to Chrome's address bar]]></title>
            <link>https://imkev.dev/crux-vis-shortcut</link>
            <guid>https://imkev.dev/crux-vis-shortcut</guid>
            <pubDate>Tue, 15 Apr 2025 11:00:00 GMT</pubDate>
            <description><![CDATA[The CrUX Vis tool is a great way to visualize the CrUX data. I created a shortcut to quickly access the tool from Chrome's address bar.]]></description>
            <content:encoded><![CDATA[<p><a href="https://cruxvis.withgoogle.com/">CrUX Vis</a> is a (experimental¹) tool that allows you to visualize and analyze the <a href="https://developers.google.com/web/tools/chrome-devtools/collect-user-metrics/crux">Chrome User Experience Report (CrUX)</a> data. The CrUX dataset provides insights from real users, including loading performance, responsiveness, and visual stability.</p>
<p><img src="https://imkev.dev/media/blog/crux-vis-web-dev-cwv.jpg" alt="Screenshot of CrUX Vis showing the CWV for web.dev"></p>
<p>The preceding image is taken from CrUX Vis and shows the Core Web Vitals of <a href="https://web.dev">https://web.dev</a>.</p>
<p>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 <a href="https://developer.chrome.com/docs/crux/dashboard">CrUX Dashboard</a> and I wanted to replicate this behavior for CrUX Vis.²</p>
<p>To create a custom Search Engine in Chrome, you need to navigate to <code>chrome://settings/search</code> and select <em>Manage search engines and site search</em>.</p>
<p><img src="https://imkev.dev/media/blog/crux-vis-search-engine.png" alt="Screenshot of Chrome's Search settings"></p>
<p>Find the section titled <em>Site Search</em> and click on the <em>Add</em> button. In the dialog, insert the following details:</p>
<ul>
<li>Search engine: <code>CrUX Vis</code></li>
<li>Shortcut: <code>cruxvis</code> (or any other unique keyword)</li>
<li>URL with %s in place of query: <code>https://cruxvis.withgoogle.com/#/?view=cwvsummary&amp;url=https%3A%2F%2F%s&amp;identifier=origin&amp;device=ALL&amp;periodStart=0&amp;periodEnd=-1&amp;display=p75s</code></li>
</ul>
<p><img src="https://imkev.dev/media/blog/crux-vis-search-engine-crux.png" alt="Screenshot of Chrome's Add Site Search"></p>
<p>You can now navigate to a CrUX Vis report directly from Chrome’s address bar. Typing <code>cruxvis</code> and pressing <code>tab</code> 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.</p>
<p><img src="https://imkev.dev/media/blog/crux-vis-search-bar.png" alt="Screenshot of Chrome's Address bar with CrUX Vis highlighted"></p>
<p>Note that the URL should not include the protocol as <code>https</code> is inferred through the URL we used when creating the custom search.</p>
<p>Thank you for reading and I hope you’ll find this useful.</p>
<hr>
<p>¹ <a href="https://developer.chrome.com/docs/crux/release-notes#202503">CrUX Vis is expected to replace the CrUX Dashboard in the coming months</a>. If you have any feedback on CrUX Vis, let the CrUX team know using <a href="https://forms.gle/kvvSDrEJYaf6APur7">the CrUX Vis survey</a> or <a href="https://groups.google.com/a/chromium.org/g/chrome-ux-report">the CrUX discussion group</a>.</p>
<p>² This post is heavily inspired by Rick Viscomi’s <a href="https://dev.to/rick_viscomi/making-a-custom-crux-dash-shortcut-in-chrome-3i4e">CrUX Dash shortcut</a>. ❤️</p>
]]></content:encoded>
            <category>Web performance</category>
            <enclosure url="https://imkev.dev/media/blog/crux-vis-search-bar.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Improving Largest Contentful Paint on slower devices]]></title>
            <link>https://imkev.dev/bimodal-distributions</link>
            <guid>https://imkev.dev/bimodal-distributions</guid>
            <pubDate>Sat, 09 Mar 2024 17:00:00 GMT</pubDate>
            <description><![CDATA[We improved Largest Contentful Paint by segmenting our users by device memory.]]></description>
            <content:encoded><![CDATA[<p>Looking at our real user metrics, we noticed that our histogram followed a <a href="https://en.wikipedia.org/wiki/Multimodal_distribution">bimodal distribution</a>. 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.</p>
<p><img src="https://imkev.dev/media/blog/bimodal-distribution.png" alt="Histogram showing Largest Contentful Paint and Page Views. The histogram has a bimodal distribution"></p>
<p>The histogram above is taken from January and the two modes are highlighted in red. After careful examination of the data available, we used <a href="https://speedcurve.com">SpeedCurve</a> to segment our data by <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory">device memory</a>. 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.</p>
<p>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.</p>
<p><img src="https://imkev.dev/media/blog/largest-contentful-paint-on-4-GB-devices-was-1.62-seconds-slower-when-compared-to-8-GB-devices.png" alt="Horizontal bar chart comparing the Largest Contentful Paint grouped by device memory"></p>
<p>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.</p>
<p>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.</p>
<p><img src="https://imkev.dev/media/blog/recalculate-style.png" alt="Screenshot from Chrome DevTools showing a 200-millisecond delay labeled Recalculate style"></p>
<p>For example, on interaction with one button, we would add <code>touch-action: none</code> to the <code>&lt;body&gt;</code> 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.</p>
<p>Over two months, we improved LCP on 4 GB devices to 3.4 seconds, reducing the gap with 8 GB devices.</p>
<p><img src="https://imkev.dev/media/blog/largest-contentful-paint-improved-by-more-than-one-second-for-4-GB-devices.png" alt="Horizontal bar chart showing the improved LCP from January to March"></p>
<p>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).</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>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.</p>
<p>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 <a href="https://www.youtube.com/watch?v=YzkdiTPxRM4">one group is irrelevant to you</a>. In any case, understanding the differences between these groups of users helps you improve the user experience for all of them.</p>
<p>Thank you for reading. Feedback is always welcome.</p>
<h2 id="notes"><a class="anchor" href="#notes">Notes</a></h2>
<ul>
<li>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.</li>
<li>All charts included in this post show data from the home page on mobile devices.</li>
<li>The data is sampled and was collected using <a href="https://speedcurve.com">SpeedCurve</a>. Visualizations are my own.</li>
<li>An <a href="https://docs.google.com/spreadsheets/d/1FonTPPJWJ23BMOB95zVxolqoxkPftsoFUUj15UlFrPA/edit#gid=500437993">export of the data is available on Google Sheets</a>.</li>
</ul>
]]></content:encoded>
            <category>Web performance</category>
            <enclosure url="https://imkev.dev/media/blog/bimodal-distribution.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Learn Performance course on web.dev]]></title>
            <link>https://imkev.dev/learn-performance</link>
            <guid>https://imkev.dev/learn-performance</guid>
            <pubDate>Wed, 01 Nov 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[I co-authored a course on web.dev covering the fundamentals of web performance.]]></description>
            <content:encoded><![CDATA[<p>Together with <a href="https://jlwagner.net/">Jeremy Wagner</a>, I am proud to have co-authored the <a href="https://web.dev/learn/performance"><strong>web.dev Learn Performance course</strong></a>.</p>
<p>The course covers <a href="https://web.dev/learn/performance/general-html-performance">introductory web performance topics</a>, including <a href="https://web.dev/learn/performance/resource-hints">resource hints</a> and <a href="https://web.dev/learn/performance/image-performance">image performance</a>, before diving into more advanced topics, such as JavaScript scheduling, <a href="https://web.dev/learn/performance/prefetching-prerendering-precaching">prefetching and prerendering</a>, and <a href="https://web.dev/learn/performance/web-worker-overview">web workers</a>.</p>
<p>Thank you to <a href="https://rachelandrew.co.uk">Rachel Andrew</a> and <a href="https://tunetheweb.com">Barry Pollard</a> for their invaluable reviews. Also, a shout-out to the entire web performance community for their continuous feedback and help. 🙏</p>
<p>The course is in its early phases. If you have <a href="https://issuetracker.google.com/issues/new?component=1400680&amp;template=1888895&amp;pli=1">discovered any bugs or would like to provide feedback or content suggestions</a>, you are welcome.</p>
]]></content:encoded>
            <category>Web performance</category>
            <category>Tutorials</category>
            <enclosure url="https://imkev.dev/media/blog/learn-performance.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Setting up a Private WebPageTest instance]]></title>
            <link>https://imkev.dev/private-wpt</link>
            <guid>https://imkev.dev/private-wpt</guid>
            <pubDate>Mon, 26 Jun 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[How to setup a private WebPageTest server and agent on an Ubuntu VM]]></description>
            <content:encoded><![CDATA[<p>This document explains the steps which were taken to set up a private WPT instance on an Ubuntu 22.04 Server.</p>
<h2 id="virtual-machine"><a class="anchor" href="#virtual-machine">Virtual Machine</a></h2>
<p>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 <a href="https://virt-manager.org/">virt-manager</a> on an Ubuntu host.</p>
<p>When setting up virt-manager, it is recommended to enable File System passthrough.</p>
<ol>
<li>Enable Shared Memory</li>
<li>Setup a File System passthrough using <code>virtiofs</code></li>
<li>After installation is complete configure it to automatically mount the shared drive:</li>
</ol>
<pre><code>$ sudo nano /etc/fstab
</code></pre>
<p>Add the following line:</p>
<pre><code class="language-sh">QEMU /mnt virtiofs rw,_netdev 0 0
</code></pre>
<p>You may also need to enable IP forwarding on your host machine if it is disabled</p>
<pre><code class="language-sh">sudo sysctl -w net.ipv4.ip_forward=1
sudo systemctl restart libvirtd
</code></pre>
<h2 id="wpt-server"><a class="anchor" href="#wpt-server">WPT Server</a></h2>
<p>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.</p>
<p><a href="https://github.com/wpo-Foundation/webpagetest/">webpagetest</a> has the following branches:</p>
<ul>
<li><code>master</code> - used on www, not for commercial use (recommended for personal use)</li>
<li><code>release</code> - rebased from <code>master</code> (might be outdated)</li>
<li><code>apache</code> - uses the Apache license. Can be used for commercial purposes</li>
</ul>
<p>To install the WebPageTest server, you can use the install script provided by webpagetest. The below script is taken from <a href="https://github.com/WPO-Foundation/wptserver-install">wptserver-install</a> but uses the <code>master</code> branch</p>
<pre><code class="language-sh">#!/bin/bash

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

# Prompt for the configuration options
echo &quot;WebPageTest automatic server install.&quot;

# 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::=&quot;--force-confdef&quot; -o Dpkg::Options::=&quot;--force-confold&quot; 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 &quot;s/%USER%/$USER/&quot; | 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 &quot;s/%USER%/$USER/&quot; | 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 &quot;s/%LOCATIONKEY%/$LOCATIONKEY/&quot; | tee /var/www/webpagetest/www/settings/settings.ini
cat wptserver-install/webpagetest/keys.ini | sed &quot;s/%SERVERSECRET%/$SERVERSECRET/&quot; | sed &quot;s/%SERVERKEY%/$SERVERKEY/&quot; | sed &quot;s/%APIKEY%/$APIKEY/&quot; | 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=&quot;*/5 * * * * curl --silent http://127.0.0.1/work/getwork.php&quot;
( crontab -l | grep -v -F &quot;$CRON_ENTRY&quot; ; echo &quot;$CRON_ENTRY&quot; ) | 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 &quot;\n&quot;
echo &quot;The location key to use when configuring agents is: $LOCATIONKEY&quot;
echo &quot;An API key to use for automated testing is: $APIKEY&quot;
</code></pre>
<p>After installation is successful, you are prompted to reboot.</p>
<p>After rebooting, navigating to the IP address of the virtual machine should show you the WebPageTest interface. Navigating to <code>/install</code> will show you a checklist of dependencies and prerequisites which were configured during the installation.</p>
<p>I also recommend setting <code>display_errors = on</code> in <code>php.ini</code> to see any PHP errors in the browser.</p>
<h2 id="wpt-agent"><a class="anchor" href="#wpt-agent">WPT Agent</a></h2>
<p>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 <code>wptserver</code>. If the WPT agent is hosted on a different machine, you just need to update the configurations.</p>
<p>You can run the following script which sets up the configurations before calling the <a href="https://github.com/WPO-Foundation/wptagent-install"><code>wptagent-install</code></a> script. The <code>WPT_KEY</code> must be taken from the WPT server, <code>/var/www/webpagetest/www/settings/settings.ini</code>.</p>
<pre><code class="language-sh">#! /bin/bash

WPT_SERVER=&quot;127.0.0.1&quot; \
WPT_LOCATION=&quot;localhost&quot; \
WPT_KEY=&quot;{WPT_KEY}&quot; \
DISABLE_IPV6=y \
WPT_BRAVE='n' \
WPT_EPIPHANY='n' \
WPT_EDGE='n' \
bash &lt;(curl -s https://raw.githubusercontent.com/WPO-Foundation/wptagent-install/master/debian.sh)
</code></pre>
<p>After the agent has finished installing, you will be prompted to reboot.</p>
<p>After rebooting, the OS will execute the script <code>~/agent.sh</code>. This script initiates the WPT agent. If you do not use HTTPS on 127.0.0.1, you may need to update the <code>agent.sh</code> script to remove <code>https</code> as per below.</p>
<pre><code class="language-sh">python3 wptagent.py -vvvv --server &quot;http://127.0.0.1/work/&quot; --location localhost --key {WPT_KEY} --exit 60 --alive /tmp/wptagent
</code></pre>
<p>You can confirm that the agent is running by accessing the nginx logs:</p>
<pre><code class="language-sh">$ tail -f /var/logs/nginx/access.log

127.0.0.1 - - [02/Mar/2023:12:16:26 +0000] &quot;GET /work/getwork.php?f=json&amp;shards=1&amp;reboot=1&amp;servers=1&amp;testinfo=1&amp;location=localhost&amp;pc=webpagetest-192.168.122.253&amp;key={WPT_KEY}&amp;version=230215.171357&amp;dns=127.0.0.53&amp;freedisk=6.641&amp;upminutes=26 HTTP/1.1&quot; 200 5 &quot;-&quot; &quot;wptagent&quot;
127.0.0.1 - - [02/Mar/2023:12:16:31 +0000] &quot;GET /work/getwork.php?f=json&amp;shards=1&amp;reboot=1&amp;servers=1&amp;testinfo=1&amp;location=localhost&amp;pc=webpagetest-192.168.122.253&amp;key={WPT_KEY}&amp;version=230215.171357&amp;dns=127.0.0.53&amp;freedisk=6.641&amp;upminutes=26 HTTP/1.1&quot; 200 5 &quot;-&quot; &quot;wptagent&quot;
127.0.0.1 - - [02/Mar/2023:12:16:36 +0000] &quot;GET /work/getwork.php?f=json&amp;shards=1&amp;reboot=1&amp;servers=1&amp;testinfo=1&amp;location=localhost&amp;pc=webpagetest-192.168.122.253&amp;key={WPT_KEY}&amp;version=230215.171357&amp;dns=127.0.0.53&amp;freedisk=6.641&amp;upminutes=26 HTTP/1.1&quot; 200 5 &quot;-&quot; &quot;wptagent&quot;
</code></pre>
<p>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 <code>--server</code> URL in the <code>wptagent.py</code> script is correct. If it returns a 500, try opening the URL in the browser and see if it shows any PHP errors.</p>
<h2 id="locations"><a class="anchor" href="#locations">Locations</a></h2>
<p>Finally, you need to configure the WPT Server to accept traffic from the WPT Agent you have just configured. This is done in <code>/var/www/webpagetest/www/settings/locations.ini</code>. For the above agent configurations, you can copy-paste the following:</p>
<pre><code>[locations]
1=Local
default=Local

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

[localhost]
browser=Chrome,Firefox
label=&quot;Ubuntu 22.04&quot;
</code></pre>
<p>Now that everything is set up, you should be able to confirm that the installation is correct by navigating to <code>/install</code>. Your agent should show up at the end of the checklist.</p>
<p><img src="https://imkev.dev/media/blog/WebPageTest-server-install.png" alt="WebPageTest 21.07 Installation Check"></p>
<h2 id="limits"><a class="anchor" href="#limits">Limits</a></h2>
<p>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 <code>/www/settings/settings.ini</code>.</p>
<pre><code>rate_limit_anon_monthly=0
rate_limit_anon=0
</code></pre>
<p>You can also set limits on the Web UI or API through the <code>/www/settings/keys.ini</code> file.</p>
<h2 id="automatic-updates"><a class="anchor" href="#automatic-updates">Automatic updates</a></h2>
<p>Personally, I recommend disabling automatic updates by commenting out the following line in <code>/www/settings/settings.ini</code>:</p>
<pre><code>gitUpdate=1
</code></pre>
<h2 id="simple-configurations"><a class="anchor" href="#simple-configurations">Simple Configurations</a></h2>
<p>To create simple configuration presets which show up on the home page, you need to create a <code>/www/settings/profiles.ini</code> file. This file will include a list of presets, including their connection, device, and location.</p>
<pre><code>[MotoG4]
label=&quot;&lt;strong&gt;&lt;b&gt;MotoG4&lt;/b&gt;&lt;/strong&gt; &lt;img src='/assets/images/test_icons/chrome.svg' alt='chrome'&gt; &lt;span class='test_presets_tag'&gt;&lt;img src='/assets/images/test_icons/signal.svg' alt=''&gt;4G&lt;/span&gt; &lt;span class='test_presets_tag'&gt;Ubuntu 22.04&lt;/span&gt;&quot;
description=&quot;Ubuntu 22.04, Simulated MotoG4, 4G Connection, 9 Mbps, 170ms RTT&quot;
location=&quot;localhost:Chrome;MotoG4.4G&quot;
runs=3
video=1
timeline=1
fvonly=1
lighthouseTrace=1
</code></pre>
<p>In the configuration above, I am creating a configuration named <code>MotoG4</code> which uses the <code>localhost</code> location with the <code>Chrome</code> browser and emulated <code>MotoG4</code> with a <code>4G</code> connection. The <code>label</code> and <code>description</code> affect how the configuration appears on the page, while the <code>runs</code>, <code>video</code>, <code>timeline</code>, <code>fvonly</code>, and <code>lighthouseTrace</code> set some default values.</p>
<p><img src="https://imkev.dev/media/blog/WebPageTest-homepage.png" alt="Screenshot of WebPageTest homepage"></p>
<p>In the screenshot above, you can see three simple configurations—iPhoneX, MotoG4, and Desktop—that were added to <code>profiles.ini</code>.</p>
<p>Happy testing!</p>
]]></content:encoded>
            <category>Web performance</category>
            <enclosure url="https://imkev.dev/media/blog/private-wpt.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[First Important Paint - Developing a custom metric]]></title>
            <link>https://imkev.dev/custom-metrics</link>
            <guid>https://imkev.dev/custom-metrics</guid>
            <pubDate>Sat, 10 Jun 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[Custom metrics allow you to extend the set of measures already available in the browser with your own measures. I explain the motivation behind First Important Paint and what I learned about developing a custom metric.]]></description>
            <content:encoded><![CDATA[<p><em>Updated: Monday Nov 27 2023</em> | <em>This article is also available in <a href="https://postd.cc/custom-metrics/">Japanese</a>.</em></p>
<p>In May 2020, Google proposed a set of <a href="https://web.dev/user-centric-performance-metrics/">user-centric</a> metrics that serve to describe a website’s performance. These are known as the <a href="https://web.dev/cwv">Core Web Vitals (CWV)</a> and include:</p>
<ul>
<li><a href="https://web.dev/lcp">Largest Contentful Paint (LCP)</a>: The time it takes to paint the largest element in the viewport.</li>
<li><a href="https://web.dev/fid">First Input Delay (FID)</a>: The delay in responding to the first user input. Soon to be replaced with <a href="https://web.dev/inp">Interaction to Next Paint (INP)</a>.</li>
<li><a href="https://web.dev/cls">Cumulative Layout Shift (CLS)</a>: A measure of how much the page content shifts.</li>
</ul>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-cwv-tech-report.png" alt="A line chart showing a blue line progressing upwards from 20% in Jan 2020 to 40% in Mar 2023"></p>
<p>As shown in the chart above extracted from <a href="https://cwvtech.report">cwvtech.report</a>, <a href="https://lookerstudio.google.com/u/0/reporting/55bc8fad-44c2-4280-aa0b-5f3f0cd3d2be/page/M6ZPC?params=%7B%22df44%22:%22include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580ALL%22%7D">the number of websites passing the CWV thresholds has increased from 22% to 40%</a>.</p>
<h2 id="the-role-of-custom-metrics"><a class="anchor" href="#the-role-of-custom-metrics">The role of custom metrics</a></h2>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-demo.png" alt="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."></p>
<p>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 <code>&lt;video&gt;</code> element with a <code>poster</code> image.</p>
<h3 id="element-timing-api"><a class="anchor" href="#element-timing-api">Element Timing API</a></h3>
<p>Similarly to LCP, the <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming">Element Timing API</a> 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 <code>elementtiming</code> attribute to an element.</p>
<p>One limitation of Element Timing is that it is <a href="https://caniuse.com/mdn-api_element_elementtiming">only supported in Chromium-based browsers</a>. Additionally, it is only supported on a <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming#description">limited subset of elements</a>.</p>
<h3 id="user-timing-api"><a class="anchor" href="#user-timing-api">User Timing API</a></h3>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing">User Timing API</a> 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.</p>
<pre><code class="language-js">// 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');
</code></pre>
<h2 id="real-user-metrics-and-lab-tests"><a class="anchor" href="#real-user-metrics-and-lab-tests">Real user metrics and lab tests</a></h2>
<p>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.</p>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-rum.png" alt="Distribution of First Important Paint values following a logarithmic normal distribution ranging from 0s to 7s"></p>
<p>The screenshot above is taken from <a href="https://speedcurve.com">SpeedCurve</a> and shows a distribution of different user experiences.</p>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-wpt.png" alt="Screenshot from WebPageTest's detail view"></p>
<p>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 <a href="https://webpagetest.org">WebPageTest</a>.</p>
<h2 id="properties-of-a-good-metric"><a class="anchor" href="#properties-of-a-good-metric">Properties of a good metric</a></h2>
<p>When presenting LCP in 2020, <a href="https://www.youtube.com/watch?v=diAc65p15ag">Paul Irish</a> described a list of 8 properties that a good metric should observe—there is also a slight variation <a href="https://chromium.googlesource.com/chromium/src/+/lkgr/docs/speed/good_toplevel_metrics.md">documented in the Chromium source code</a>:</p>
<ul>
<li>Representative: Correlates with user experience and business value.</li>
<li>Interpretable: The metric and its value are easy to understand.</li>
<li>Accurate: How closely the metric measures what it is expected to measure.</li>
<li>Simple: The method it is computed is not complex.</li>
<li>Orthogonal: It should not be measuring something that is already being measured by other metrics.</li>
<li>Stable: There shouldn’t be a lot of variance between runs.</li>
<li>Elastic: A small change in the input results in a small change in the metric.</li>
<li>Correlates: The metric should correlate well with other metrics and between lab/field tests.</li>
</ul>
<h3 id="observer-effect"><a class="anchor" href="#observer-effect">Observer Effect</a></h3>
<p>Another important property of a good metric is that it does not influence the test itself. In science, the <a href="https://en.wikipedia.org/wiki/Observer_effect_(physics)">Observer effect</a> 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.</p>
<h2 id="first-important-paint"><a class="anchor" href="#first-important-paint">First Important Paint</a></h2>
<p><a href="https://github.com/kevinfarrugia/first-important-paint">First Important Paint (FIP)</a> is a custom metric that was developed to measure the time it takes to paint the first annotated HTML element onto the page.</p>
<p>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.</p>
<p><em>(It also comes with a three-letter acronym so you know it’s legit 😛)</em></p>
<h3 id="is-fip-a-good-metric"><a class="anchor" href="#is-fip-a-good-metric">Is FIP a good metric?</a></h3>
<p>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.</p>
<h4 id="representative"><a class="anchor" href="#representative">Representative</a></h4>
<p>To test if FIP is representative, I prepared some <a href="https://www.webpagetest.org/result/230609_BiDc8Q_1d6bbfd5da548a2bb04076a5c6bf1438/2/details/">lab tests</a> 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.</p>
<h4 id="interpretable"><a class="anchor" href="#interpretable">Interpretable</a></h4>
<p>The First Important Paint is easy to understand and is measured in milliseconds.</p>
<h4 id="accurate"><a class="anchor" href="#accurate">Accurate</a></h4>
<p>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.</p>
<p>The tests were executed for <a href="https://docs.google.com/spreadsheets/d/1ola_2GGd1NWOqeVJ_Ea1CGbLn2DvcGNHDI2P43xgtjg/edit?usp=sharing#gid=1072306481">image</a> and <a href="https://docs.google.com/spreadsheets/d/1ola_2GGd1NWOqeVJ_Ea1CGbLn2DvcGNHDI2P43xgtjg/edit?usp=sharing#gid=2063136168">text</a> 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.</p>
<p>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.</p>
<p>The reasons for the inaccuracy are various and are limitations in how the metric is computed.</p>
<h4 id="simple"><a class="anchor" href="#simple">Simple</a></h4>
<p>The <a href="https://github.com/kevinfarrugia/first-important-paint">code for measuring and computing FIP</a> is simple and can easily be debugged or modified if needed.</p>
<h4 id="orthogonal"><a class="anchor" href="#orthogonal">Orthogonal</a></h4>
<p>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 <em>largest</em> element, in which case it would overlap with LCP.</p>
<h4 id="stable"><a class="anchor" href="#stable">Stable</a></h4>
<p>To measure the stability of First Important Paint, I ran lab tests on 3 different connection speeds.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-stable.png" alt="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."></p>
<p>As shown in the chart above, all connections experienced very little variation between one test and another.</p>
<h4 id="elastic"><a class="anchor" href="#elastic">Elastic</a></h4>
<p>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.</p>
<p><a href="https://www.webpagetest.org/result/230609_BiDc6F_4f0c2f5d8d52b79857b016b539cc2ebe/3/details/">One of the earlier iterations of FIP did not record any improvements after this change</a>. 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.</p>
<p>After this issue was resolved <a href="https://www.webpagetest.org/video/compare.php?tests=230609_BiDc8Q_1d6bbfd5da548a2bb04076a5c6bf1438-l:Original,230609_AiDcZW_f90f83a17cae800f8fc845dea6c8fd38-l:Optimized-e:filmstrip">we can see a small and proportional improvement in FIP</a> that matches the improved user experience.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-elastic.png" alt="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."></p>
<p>In the comparison above, the <a href="https://www.webpagetest.org/result/230609_BiDc8Q_1d6bbfd5da548a2bb04076a5c6bf1438/2/details/#waterfall_view_step1">Original test</a> reported an LCP of 1.315s and FIP of 1.470s. The <a href="https://www.webpagetest.org/result/230609_AiDcZW_f90f83a17cae800f8fc845dea6c8fd38/1/details/">Optimized test</a>—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.</p>
<h4 id="correlates"><a class="anchor" href="#correlates">Correlates</a></h4>
<p>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 <a href="https://en.wikipedia.org/wiki/Coefficient_of_determination">coefficient of determination</a>. 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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-correlates-lcp.png" alt="Scatter plot showing a First Important Paint on the horizontal axis and Largest Contentful Paint on the vertical axis with a positive correlation."></p>
<p>For text elements, Regular 4G recorded a strong correlation while Regular 3G did not register an equally strong correlation.</p>
<p>Using real user data available in SpeedCurve we can also check if FIP correlates to business metrics.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-correlates-bounce-rate.png" alt="A correlation chart showing a blue line representing Bounce Rate increasing as FIP increases."></p>
<p>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.</p>
<h4 id="observer-effect-2"><a class="anchor" href="#observer-effect-2">Observer Effect</a></h4>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/first-important-paint-observer-effect.png" alt="A line chart showing blue and red dots placed in line with each other representing the set of tests executed with and without FIP."></p>
<p>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 <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming">long tasks</a> in addition to LCP.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>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.</p>
<p>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.</p>
<p>Thank you for reading. I’m always eager to learn, so feedback and questions are welcome.</p>
]]></content:encoded>
            <category>Web performance</category>
            <category>Web development</category>
            <enclosure url="https://imkev.dev/media/blog/custom-metrics.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Web Performance Audit]]></title>
            <link>https://imkev.dev/performance-audit</link>
            <guid>https://imkev.dev/performance-audit</guid>
            <pubDate>Wed, 07 Jun 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[I was asked to perform a performance audit for an online casino. This article is a redacted version of the report I have delivered to the client detailing my findings and recommendations.]]></description>
            <content:encoded><![CDATA[<p><em>Updated: Wednesday July 12 2023</em></p>
<p>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.</p>
<p>My first step when starting a performance audit is to understand the current situation and set up a baseline. Relying on <a href="https://pagespeed.web.dev/">Lighthouse</a> 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 <a href="https://www.speedcurve.com/">SpeedCurve</a>. Alternatively, you could use the RUM data already available in <a href="https://developer.chrome.com/docs/crux/">CrUX</a>.</p>
<p>We then configured some lab tests on <a href="https://webpagetest.org">WebPageTest</a>. 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.</p>
<p>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 <a href="https://developer.chrome.com/docs/crux/methodology/#eligibility">CrUX eligibility criteria</a>.</p>
<h2 id="largest-contentful-paint-lcp"><a class="anchor" href="#largest-contentful-paint-lcp">Largest Contentful Paint (LCP)</a></h2>
<p>The first thing we noted is that the <a href="https://web.dev/lcp">LCP</a> element on the home page is a placeholder image and not representative of the user experience.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-lcp-element.png" alt="Dark grey background with a light grey loading spinner"></p>
<p>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.</p>
<p>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 <a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/speed/metrics_changelog/2023_04_lcp.md">bits-per-pixel</a> 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 <a href="https://github.com/kevinfarrugia/first-important-paint">First Important Paint</a>.</p>
<p>The new LCP image is the 79th requested element. It is a late-discovered resource—such as a CSS <code>background-image</code> or an element added to the DOM using JavaScript. For this client, while the <code>&lt;img&gt;</code> element is present in the initial document, the <code>src</code> attribute is populated using a JavaScript lazy-loading library. Lazy-loading the LCP element is an anti-pattern and <a href="https://web.dev/lcp-lazy-loading/">can delay rendering</a>.</p>
<h2 id="cumulative-layout-shift-cls"><a class="anchor" href="#cumulative-layout-shift-cls">Cumulative Layout Shift (CLS)</a></h2>
<p>The main contributor to <a href="https://web.dev/cls">CLS</a> is a UI issue in the slider which appears on the home page.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-cls.png" alt="Filmstrip showing the loading of the hero image"></p>
<p>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 <code>0px</code> until it reaches the full viewport width.</p>
<p>To test my hypothesis, we added <code>width: 100% !important</code> to the <code>.slider-slide</code> CSS selector using <a href="https://product.webpagetest.org/experiments">WebPageTest experiments</a>, forcing the slider’s contents to always take up the full viewport width rather than transition from <code>0px</code>. This eliminated the layout shift from the home page banner content resulting in an improvement from 1.20 to 0.17.</p>
<p>The second largest contributor to the CLS score is a layout shift of the footer.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-cls-footer.png" alt="Screenshot of homepage showing the footer within the initial viewport"></p>
<p>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.</p>
<h2 id="time-to-first-byte-ttfb"><a class="anchor" href="#time-to-first-byte-ttfb">Time to First Byte (TTFB)</a></h2>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-ttfb.png" alt="Grouped horizontal bar chart showing three test runs"></p>
<p>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.</p>
<p>I advised the development team to set up monitoring on the server and include <code>server-timing</code> 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 <code>node-cache</code> and will be re-implemented to use a “stale-while-revalidate” strategy.</p>
<h2 id="document-object-model-dom"><a class="anchor" href="#document-object-model-dom">Document Object Model (DOM)</a></h2>
<p>The home page has a <a href="https://www.debugbear.com/html-size-analyzer">large number of DOM nodes</a> which can affect load time and interactivity.</p>
<pre><code class="language-js">document.querySelectorAll('*').length; // 3704
</code></pre>
<p>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.</p>
<pre><code class="language-html">&lt;div class=&quot;Description&quot;&gt;
  &lt;p&gt;
    &lt;span style=&quot;font-size: 20px;&quot;&gt;
      &lt;strong
        &gt;The
        &lt;span style=&quot;color: #ffe4b5;&quot;&gt;Lorem Ipsum&lt;/span&gt;
      &lt;/strong&gt;
    &lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
</code></pre>
<p>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>
<pre><code class="language-html">&lt;p class=&quot;Description&quot; style=&quot;font-size:20px; font-weight: bold&quot;&gt;
  The &lt;span style=&quot;color: #ffe4b5;&quot;&gt;Lorem Ipsum&lt;/span&gt;
&lt;/p&gt;
</code></pre>
<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.</p>
<p>A good example on this page is the <em>PlayButton</em> <code>&lt;span&gt;</code> element that includes an <code>&lt;svg&gt;</code> 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 <code>&lt;img&gt;</code> element or a CSS <code>background-image</code>. 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.</p>
<h2 id="javascript"><a class="anchor" href="#javascript">JavaScript</a></h2>
<p>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.</p>
<p>The website is developed using React so it is possible to implement code-splitting using <a href="https://react.dev/reference/react/lazy"><code>React.lazy</code></a>.</p>
<p>The page also downloads a JavaScript file <code>/static/js/svg-0.js</code> 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 <code>svg-url-loader</code> or <code>url-loader</code> to <code>file-loader</code>. Jacob Grob has written a <a href="https://kurtextrem.de/posts/svg-in-js">detailed article</a> on this topic.</p>
<p>The page also has a single render-blocking script <code>https://cdn.trackjs.com/agent/v3/latest/t.js</code> 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 <code>defer</code> attribute?</p>
<h2 id="images"><a class="anchor" href="#images">Images</a></h2>
<p>The website uses a JavaScript library for image lazy-loading. We can replace this with the browser’s <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading">native image lazy-loading</a> which is <a href="https://caniuse.com/loading-lazy-attr">available in all major browsers</a>. Additionally, all images are being lazy-loaded, including those in the initial viewport. <a href="https://web.dev/lcp-lazy-loading/">Only use <code>loading=&quot;lazy&quot;</code> for images outside the initial viewport</a>. 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.</p>
<p>The page has a large number of images in different formats. <a href="https://caniuse.com/webp">WebP is supported on all major browsers</a> 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.</p>
<p>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.</p>
<p>Secondly, <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1458806#c10">SVG animations do not run on the compositor thread unless the device has a DPR of 1.0 and the SVG is inlined</a>. This page includes all SVG animations using the <code>&lt;img&gt;</code> tag which causes CPU (and GPU) usage to increase significantly.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-svg-animation.png" alt="Screenshot from Chrome's performance panel showing frequent spikes in layout and paint"></p>
<p>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.</p>
<p>Finally, we can improve the cacheability of the banner images by increasing the <code>max-age</code> to <code>31536000</code> (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 <code>max-age</code> of <code>200</code>.</p>
<h2 id="fonts"><a class="anchor" href="#fonts">Fonts</a></h2>
<p>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 <code>max-age</code> for all three font files can be increased to <code>31536000</code>—1 year.</p>
<h2 id="browser-hints"><a class="anchor" href="#browser-hints">Browser Hints</a></h2>
<h3 id="fetch-priority-api"><a class="anchor" href="#fetch-priority-api">Fetch Priority API</a></h3>
<p>The <a href="https://web.dev/fetch-priority">Fetch Priority API</a> is supported on Chrome and will soon be enabled on Safari. You can add <code>fetchpriority=&quot;high&quot;</code> to prioritize critical assets. Consider adding <code>fetchpriority=&quot;high&quot;</code> on the first banner image (the LCP element) and the <code>&lt;script&gt;</code> files.</p>
<h3 id="preload"><a class="anchor" href="#preload">Preload</a></h3>
<p>The page makes use of some <code>&lt;link rel=&quot;preload&quot;&gt;</code> elements to instruct the browser to fetch JavaScript and CSS files. Considering that these files are located in the document <code>&lt;head&gt;</code> and are not late-discovered resources, we can remove the <code>preload</code> for CSS and script files.</p>
<p>Some <code>&lt;script&gt;</code> tags are located before the closing <code>&lt;body&gt;</code> tag. These could be moved to the <code>&lt;head&gt;</code> and the <code>defer</code> attribute added to each of them to make sure that they are not render-blocking.</p>
<h3 id="preconnect"><a class="anchor" href="#preconnect">Preconnect</a></h3>
<p>The page is dependent on the <code>storage.googleapis.com</code> and <code>static.everymatrix.com</code> domains for showing critical content—including the LCP image. Preconnecting to these domains using a <code>&lt;link rel=&quot;preconnect&quot;&gt;</code> element can reduce the time it takes to download these resources by opening a connection earlier.</p>
<h2 id="title"><a class="anchor" href="#title"><code>&lt;title&gt;</code></a></h2>
<p>In most cases, the <code>&lt;title&gt;</code> should be placed as high as possible within the document’s <code>&lt;head&gt;</code> as this gives the user immediate feedback on the content of the page. The <code>&lt;title&gt;</code> element should be placed beneath any <code>&lt;meta charset|http-equiv|viewport&gt;</code> elements. This won’t affect any metrics but it is a good practice to follow.</p>
<h2 id="legacy-code"><a class="anchor" href="#legacy-code">Legacy code</a></h2>
<p>The page contains some HTML elements specific to Internet Explorer that can be safely removed. This includes an <code>http-equiv=&quot;X-UA-Compatible&quot;</code> meta element and a few conditional comments—such as <code>&lt;!--[if lt IE 7]&gt;</code>.</p>
<p>The page also uses an old pattern where the CSS class <code>no-js</code> is added to the <code>&lt;html&gt;</code> element to recognize if the page is interactive—the <code>no-js</code> class is usually removed as soon as JavaScript is executed. However, adding and removing CSS classes to the <code>&lt;html&gt;</code> element causes a <a href="https://web.dev/reduce-the-scope-and-complexity-of-style-calculations/">Recalculate Style</a> which can slow down the page. This can be safely removed.</p>
<h2 id="server-side-rendering"><a class="anchor" href="#server-side-rendering">Server-side rendering</a></h2>
<p>It is unclear what the intended role of the server-rendered content on the home page is.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-splash-screen.jpg" alt="Splash screen for the homepage"></p>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/performance-audit-disabled-splash-screen.png" alt="Screenshot of homepage with JavaScript disabled"></p>
<p>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 <code>display: none</code> with the current loading experience. The experiment is a full second faster to show the game grid and meaningful content.</p>
<h2 id="going-forward"><a class="anchor" href="#going-forward">Going forward</a></h2>
<p>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.</p>
<p>Thank you for reading. Please get in touch if you have any questions or feedback.</p>
<hr>
<p><em>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.</em></p>
]]></content:encoded>
            <category>Web performance</category>
            <enclosure url="https://imkev.dev/media/blog/performance-audit.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Fetch Priority and optimizing LCP]]></title>
            <link>https://imkev.dev/fetchpriority-opportunity</link>
            <guid>https://imkev.dev/fetchpriority-opportunity</guid>
            <pubDate>Mon, 02 Jan 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[Fetch Priority is used to indicate to the browser the relative priority of a resource. I take a deep dive into how you can improve your Largest Contentful Paint by using fetchpriority.]]></description>
            <content:encoded><![CDATA[<p><em>Updated: Tuesday May 02 2023</em> | <em>This article is also available in <a href="https://postd.cc/fetchpriority-opportunity/">Japanese</a>.</em></p>
<p>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 <code>fetchpriority</code> attribute to <code>&lt;img&gt;</code>, <code>&lt;link&gt;</code>, <code>&lt;script&gt;</code>, and <code>&lt;iframe&gt;</code> elements or through the <code>priority</code> attribute on the <a href="https://fetch.spec.whatwg.org/#fetch-method"><code>Fetch</code></a> API.</p>
<p>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 <code>&lt;head&gt;</code> will be assigned the <code>Highest</code> priority, while a <code>&lt;script&gt;</code> element with the <code>defer</code> attribute will be assigned the <code>Low</code> priority. The browser downloads resources with the same priority in the order in which they are discovered.</p>
<h2 id="fetchpriority"><a class="anchor" href="#fetchpriority"><code>fetchpriority</code></a></h2>
<p>The <a href="https://wicg.github.io/priority-hints/#definitions"><code>fetchpriority</code></a> 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:</p>
<ul>
<li><code>high</code> - The resource is more important relative to its default priority</li>
<li><code>low</code> - The resource is less important relative to its default priority</li>
<li><code>auto</code> - The default value</li>
</ul>
<pre><code class="language-html">&lt;img src=&quot;/lcp.jpg&quot; alt=&quot;A dog&quot; fetchpriority=&quot;high&quot; /&gt;
</code></pre>
<p>In the example above, we are hinting to the browser that the <code>&lt;img&gt;</code> priority is more important than its default priority.</p>
<p>The same values are supported for the <a href="https://fetch.spec.whatwg.org/#dom-requestinit-priority"><code>priority</code></a> attribute on the <code>fetch</code> method.</p>
<pre><code class="language-js">fetch(&quot;/api/data.json&quot;, { priority: 'high' })
</code></pre>
<p>In the <code>fetch</code> request above, we are indicating to the browser that the <code>fetch</code> request has an increased priority compared to its default priority.</p>
<h2 id="default-priority"><a class="anchor" href="#default-priority">Default priority</a></h2>
<p>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 <code>Low</code> priority. Assigning <code>fetchpriority=&quot;high&quot;</code> will increase their priority to <code>High</code>. On the other hand, a render-blocking stylesheet is assigned a <code>Highest</code> priority by default. Assigning it <code>fetchpriority=&quot;low&quot;</code> will lower its priority to <code>High</code> - but not <code>Low</code>. <code>fetchpriority</code> is used to adjust a resource’s priority relative to its default, rather than to explicitly set its value.</p>
<p>The <a href="https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc/edit">influence of Fetch Priority on resource prioritization in Chromium</a> documents the different resource types, their default priority (◉), and the resultant priority when using <code>fetchpriority=&quot;high&quot;</code> (⬆) and <code>fetchpriority=&quot;low&quot;</code> (<strong>⬇</strong>).</p>
<p><em>Note that if an image is discovered to be within the viewport, then its priority is boosted to <code>High</code>. However, this could be quite late in the loading process and may have little or no impact if the request was already sent. Using <code>fetchpriority=&quot;high&quot;</code> allows you to tell the browser to start in <code>High</code> priority, rather than waiting for the browser to find out if it is in the viewport or not.</em></p>
<h2 id="tight-mode"><a class="anchor" href="#tight-mode">“Tight mode”</a></h2>
<p>Most browsers download resources in two phases. During the initial phase (Chromium also refers to this as “Tight mode”), the browser does not download <code>Low</code> priority resources unless there are less than two in-flight requests.</p>
<p><img src="https://imkev.dev/media/blog/tight-mode.png" alt="WebPageTest waterfall chart illustrating the initial phase"></p>
<p>In the waterfall chart above, you could see that the resource <code>image-1.jpg</code> does not start downloading until <code>style-2.css</code> has finished downloading - even if it was discovered immediately. At this point, only one resource remains in-flight - <code>script.js</code>, so the browser begins to download the <code>Low</code> priority image.</p>
<p>The initial phase is completed once all blocking scripts in the <code>&lt;head&gt;</code> have been downloaded and executed (scripts with <code>async</code> or <code>defer</code> 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.</p>
<p><img src="https://imkev.dev/media/blog/interactive.png" alt="WebPageTest waterfall chart illustrating DOM Interactive"></p>
<p>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 <code>readystatechange</code> event was fired.</p>
<h2 id="preconnect"><a class="anchor" href="#preconnect">preconnect</a></h2>
<p>If the images reside on a separate domain, the browser needs to open a connection to the domain before downloading the files.</p>
<p><img src="https://imkev.dev/media/blog/crossorigin-images.png" alt="WebPageTest waterfall chart illustrating crossorigin images"></p>
<p>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 <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preconnect"><code>preconnect</code></a> resource hint.</p>
<p><img src="https://imkev.dev/media/blog/preconnect.png" alt="WebPageTest waterfall chart illustrating  resource hint"></p>
<p>In the chart above, the connection to the <code>cdn.glitch.global</code> 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.</p>
<h2 id="preload"><a class="anchor" href="#preload"><code>preload</code></a></h2>
<p>If we were able to improve the download time using the <code>preconnect</code> resource hint, are we able to improve it further using the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload"><code>preload</code></a> directive? Short answer: no. The <code>preload</code> 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.</p>
<p><img src="https://imkev.dev/media/blog/preload.png" alt="WebPageTest waterfall chart illustrating  directive"></p>
<p>In the chart above, we have replaced the <code>preconnect</code> hint with the following:</p>
<pre><code class="language-html">&lt;link
  rel=&quot;preload&quot;
  as=&quot;image&quot;
  href=&quot;https://cdn.glitch.global/.../image-1.jpg&quot;
/&gt;
</code></pre>
<p>Despite the preload, the image still doesn’t begin downloading until there are less than two requests in-flight.</p>
<h2 id="fetchpriority-2"><a class="anchor" href="#fetchpriority-2"><code>fetchpriority</code></a></h2>
<p>We can use Fetch Priority to indicate to the browser that <code>image-1.jpg</code> is more important than its default priority using:</p>
<pre><code class="language-html">&lt;img
  src=&quot;https://cdn.glitch.global/.../image-1.jpg&quot;
  fetchpriority=&quot;high&quot;
  alt=&quot;&quot;
/&gt;
</code></pre>
<p>This should increase the initial priority of the image from <code>Low</code> to <code>High</code>, allowing the image to be picked up in the initial phase.</p>
<p><img src="https://imkev.dev/media/blog/fetchpriority.png" alt="WebPageTest waterfall chart illustrating "></p>
<p>The waterfall chart above shows that <code>image-1.jpg</code> is picked up during the initial phase, in parallel with the other critical resources. This gives us the greatest improvement so far.</p>
<h2 id="firefox"><a class="anchor" href="#firefox">Firefox</a></h2>
<p>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 <code>Low</code> priority resources until all JavaScript in the <code>&lt;head&gt;</code> is downloaded and executed - even when there is only one <code>High</code> priority request in-flight.</p>
<p><img src="https://imkev.dev/media/blog/firefox-tight-mode.png" alt="Screenshot from Firefox Web Developer Tools illustrating the initial phase"></p>
<p>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 <code>interactive</code> - vertical, blue line.</p>
<p><em>While Chrome waits for JavaScript declared in the <code>&lt;head&gt;</code> 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 <code>&lt;head&gt;</code>.</em></p>
<p>Firefox does not <a href="https://github.com/whatwg/html/issues/7150#issuecomment-1299923593">support <code>fetchpriority</code></a> yet, however, we can increase the priority of <code>image-1.jpg</code> using the <code>preload</code> directive.</p>
<p><img src="https://imkev.dev/media/blog/firefox-preload-and-fetchpriority.png" alt="Screenshot from Firefox Web Developer Tools illustrating the initial phase"></p>
<p>In the screenshot above, the file <code>image-1.jpg</code> is fetched in parallel with the other resources. This is similar to the behavior we have seen when adding <code>fetchpriority=&quot;high&quot;</code> on Google Chrome.</p>
<h2 id="safari"><a class="anchor" href="#safari">Safari</a></h2>
<p>Safari on iOS and macOS also has an initial phase although it behaves differently than Chrome and Firefox.</p>
<p><code>Low</code> priority resources start being fetched when there are fewer than two in-flight requests. It is not dependent on the <code>readystatechange</code> event and even on pages without any render-blocking JavaScript, the browser will wait until there is one in-flight request.</p>
<p><img src="https://imkev.dev/media/blog/safari-tight-mode.png" alt="Screenshot showing Safari Web Inspector tight mode"></p>
<p>In the screenshot above, taken from Safari’s Web Inspector, the images do not start downloading until <code>style-1.css</code> finishes downloading and there are less than two in-flight requests.</p>
<p>On Safari, the initial phase only applies to resources from the same origin. If the <code>Low</code> priority resources are loaded from a different domain they will be fetched as soon as they are discovered.</p>
<p><img src="https://imkev.dev/media/blog/safari-crossorigin.png" alt="Screenshot showing Safari Web Inspector not restricting crossorigin  priority requests"></p>
<p>In the screenshot above, the crossorigin images are fetched immediately without waiting for the <code>High</code> priority resources to finish downloading.</p>
<p>The <code>preload</code> directive does not affect the resource’s priority. However, placing the <code>&lt;link rel=&quot;preload&quot;&gt;</code> directive before <code>High</code> 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 <code>preload</code> directives above <code>High</code> priority resources as render-blocking CSS should take precedence.</p>
<p><img src="https://imkev.dev/media/blog/safari-preload.png" alt="Screenshot of Safari Web Inspector illustrating the  directive"></p>
<p>In this screenshot, the <code>Low</code> priority file <code>image-1.jpg</code> begins downloading before the <code>High</code> priority <code>style-1.css</code> file because the <code>&lt;link rel=&quot;preload&quot;&gt;</code> is placed above it in the document markup.</p>
<h2 id="combining-preload-with-fetchpriority"><a class="anchor" href="#combining-preload-with-fetchpriority">Combining <code>preload</code> with <code>fetchpriority</code></a></h2>
<p>Fetch Priority is only <a href="https://caniuse.com/?search=fetchpriority">supported on Chromium-based browsers</a> so far, however, it fails gracefully on unsupported browsers that do not recognize the <code>fetchpriority</code> attribute. This allows us to combine the <code>preload</code> directive with Fetch Priority.</p>
<pre><code class="language-html">&lt;link
  rel=&quot;preload&quot;
  as=&quot;image&quot;
  fetchpriority=&quot;high&quot;
  href=&quot;https://cdn.glitch.global/.../image-1.jpg&quot;
/&gt;
</code></pre>
<p>Browsers that support Fetch Priority will preload the resource using the assigned <code>fetchpriority</code>, while browsers that do not will use the <code>preload</code> directive.</p>
<p><img src="https://imkev.dev/media/blog/preload-and-fetchpriority.png" alt="WebPageTest waterfall chart showing  and "></p>
<p>The above chart shows similar results to the one earlier which included the <code>fetchpriority</code> attribute on the <code>&lt;img&gt;</code> 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.</p>
<h2 id="fetchpriority-all-the-things"><a class="anchor" href="#fetchpriority-all-the-things"><code>fetchpriority</code> all the things</a></h2>
<p>In this section, we will look at the potential benefit of using <code>fetchpriority</code>. All data is taken from the <a href="https://httparchive.org">HTTP Archive</a> and we are only considering pages that use HTTP/2 or HTTP/3 and where the <a href="https://web.dev/lcp">Largest Contentful Paint</a> (LCP) element is an image. All <a href="https://github.com/kevinfarrugia/fetchpriority-opportunity">queries</a> and <a href="https://docs.google.com/spreadsheets/d/11QwVrr1zcFGBhOMNe25uFBD9VUfg4JP3AUkhWjod9RY/edit?usp=sharing">results</a> are publicly available.</p>
<p><em>Note: The HTTP Archive data is collected using a private instance of WebPageTest using Chrome. You can learn more about their <a href="https://httparchive.org/faq#how-is-the-data-gathered">methodology</a>.</em></p>
<p><img src="https://imkev.dev/media/blog/opportunity.png" alt="WebPageTest waterfall chart illustrating the opportunity for the LCP image with a horizontal red line"></p>
<p>I am assuming the benefit from <code>fetchpriority</code> as the difference between the time the resource is discovered and the time it starts downloading. I refer to this as the <em>opportunity</em>. Therefore if a resource is discovered early but the browser starts downloading it late, then the opportunity is greater.</p>
<p><em>Note that if the images are served from a different domain, I am including the time to open the connection in the opportunity.</em></p>
<p><img src="https://imkev.dev/media/blog/opportunity-vs-largest-contentful-paint.png" alt="Combination chart showing opportunity vs LCP"></p>
<p>The chart above plots the <em>opportunity</em> (in milliseconds) against the LCP. The <em>opportunity</em> 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 <em>opportunity</em> and the LCP - the greater the <em>opportunity</em>, the worse the LCP.</p>
<p><img src="https://imkev.dev/media/blog/opportunity-by-initial-priority.png" alt="Bar chart showing distribution of opportunity by initial priority"></p>
<p>The above chart shows the distribution of the <em>opportunity</em> for mobile devices for <code>Low</code> and <code>High</code> priority. At the median, an LCP image requested with <code>High</code> priority starts to be downloaded 21ms after it is discovered, while an LCP image with <code>Low</code> priority is downloaded after 102ms. The difference grows even further at the 75th and 90th percentile.</p>
<p><em>In addition to <code>fetchpriority=&quot;High&quot;</code>, an image may have an initial <code>High</code> priority if the image is late-discovered, for example when using CSS <code>background-image</code> or adding an image using JavaScript. In these cases, <code>fetchpriority</code> would not help since the request already has a <code>High</code> priority.</em></p>
<p>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 <code>Low</code> priority resources are not fetched immediately when there is at least one render-blocking script and two or more in-flight requests.</p>
<p><img src="https://imkev.dev/media/blog/no-of-render-blocking-resources-vs-median-opportunity.png" alt="Combination chart showing the number of render-blocking resources vs median opportunity"></p>
<p>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.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>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.</p>
<p><img src="https://imkev.dev/media/blog/distribution-of-opportunity.png" alt="Distribution of opportunity"><br>
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.</p>
<p>There are also case studies showing an improvement to <a href="https://web.dev/lcp">Largest Contentful Paint</a> (LCP) after adding <code>fetchpriority=&quot;high&quot;</code> to the LCP image. <a href="https://www.etsy.com/codeascraft/priority-hints-what-your-browser-doesnt-know-yet">Etsy saw a 4% improvement</a>, some others reportedly saw 20-30% improvements.</p>
<p>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.</p>
<p>In a nutshell,</p>
<ul>
<li>Host your LCP image on the same domain as your HTML document. If this is not possible, use <code>preconnect</code> to open an early connection.</li>
<li>The LCP image should be part of the document markup. If you are unable to do this, use <code>preload</code> to tell the browser to download the image before it is requested.</li>
<li>Avoid blocking resources when possible. If your LCP image is downloaded with a <code>Low</code> priority, use <code>fetchpriority</code> to hint the browser to download your image earlier.</li>
<li>You can use <code>preload</code> to prioritize your LCP image on Firefox until <code>fetchpriority</code> is supported. Safari does not download images earlier when using the <code>preload</code> directive.</li>
</ul>
<p>Let me know what you think. Your feedback is welcome. ♥</p>
<h2 id="related-links"><a class="anchor" href="#related-links">Related links</a></h2>
<ul>
<li><a href="https://fetchpriority-opportunity.glitch.me/">Demos</a></li>
<li><a href="https://github.com/kevinfarrugia/fetchpriority-opportunity">Queries</a> &amp; <a href="https://docs.google.com/spreadsheets/d/11QwVrr1zcFGBhOMNe25uFBD9VUfg4JP3AUkhWjod9RY/edit?usp=sharing">Results</a></li>
<li><a href="https://web.dev/priority-hints/">Optimizing resource loading with Priority Hints</a> (web.dev)</li>
<li><a href="https://www.debugbear.com/blog/priority-hints">Prioritizing Important Page Resources With Priority Hints</a> (<a href="http://debugbear.com">debugbear.com</a>)</li>
</ul>
<h2 id="special-thanks"><a class="anchor" href="#special-thanks">Special thanks</a></h2>
<p>Special thanks to <a href="https://webperf.social/@tunetheweb">Barry Pollard</a> for his advice and valuable feedback.</p>
<h2 id="notes"><a class="anchor" href="#notes">Notes</a></h2>
<ul>
<li>This feature was originally called Priority Hints but was renamed to Fetch Priority after standardization.</li>
</ul>
]]></content:encoded>
            <category>Web performance</category>
            <category>Core Web Vitals</category>
            <category>Web development</category>
            <enclosure url="https://imkev.dev/media/blog/fetchpriority-opportunity.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The 2022 Web Almanac - JavaScript, Third Parties and Interop 2022]]></title>
            <link>https://imkev.dev/2022-web-almanac</link>
            <guid>https://imkev.dev/2022-web-almanac</guid>
            <pubDate>Tue, 27 Sep 2022 12:00:00 GMT</pubDate>
            <description><![CDATA[I contributed to the latest edition of the Web Almanac, the annual publication curated by industry experts analyzing the current state of the web.]]></description>
            <content:encoded><![CDATA[<p>For the second year running, I have contributed to the latest edition of the HTTP Archive’s <a href="https://almanac.httparchive.org/en/2022/">Web Almanac</a>. This year I did not author any chapters, but I was involved as an analyst and reviewer on three chapters:</p>
<ul>
<li><a href="https://almanac.httparchive.org/en/2022/javascript">JavaScript</a></li>
<li><a href="https://almanac.httparchive.org/en/2022/third-parties">Third Parties</a></li>
<li><a href="https://almanac.httparchive.org/en/2022/interoperability">Interoperability</a></li>
</ul>
<p>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.</p>
<h2 id="interoperability"><a class="anchor" href="#interoperability">Interoperability</a></h2>
<p>Interoperability is a new addition to the Web Almanac and complements the <a href="https://web.dev/interop-2022/">Interop 2022</a> initiative that aims to bring the major browser vendors together to resolve compatibility issues and improve the developer experience in some key areas.</p>
<p>One of these key areas is <a href="https://almanac.httparchive.org/en/2022/interoperability#cascade-layers">cascade layers</a> 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 <code>@layer</code> at-rule, we can establish our layers and specificity will only apply within each layer.</p>
<pre><code>@layer site {
  .my-single_class {
    color: rebeccapurple;
  }
}
</code></pre>
<p>As this feature is still very new, only <strong>0.003%</strong> of pages in the dataset contained an <code>@layer</code> ruleset, but hopefully, the work of the Interop 2022 initiative pays off and we see greater adoption soon.</p>
<h2 id="javascript"><a class="anchor" href="#javascript">JavaScript</a></h2>
<p>The amount of JavaScript being downloaded on mobile devices has increased by <strong>8%</strong> 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.</p>
<p class="centered"><a href="https://almanac.httparchive.org/en/2022/javascript#transpilers"><img src="https://imkev.dev/media/blog/web-almanac-2022-babel-rank.png" alt="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." title="Pages using Babel grouped by rank"></a></p>
<p class="caption">Pages using Babel grouped by rank</p>
<p>For the first time, we are parsing source maps to better understand how the JavaScript we are shipping is built, with webpack bundling <strong>17%</strong> of the top 1,000 sites and Babel transpiling <strong>40%</strong> of the top 1,000 websites and <strong>26%</strong> 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.</p>
<p>In addition to how we are compiling and serving JavaScript, the JavaScript we write could also require some attention, with <strong>67%</strong> of pages shipping legacy JavaScript, <strong>18%</strong> using the unfavorable <code>document.write</code>, and <strong>2.5%</strong> still using synchronous XHR. While newer and more performant methods, such as the Scheduler API only see <strong>0.002%</strong> adoption so far.</p>
<p>As the web platform matures, we hope to see increased adoption of these modern APIs and an improved and faster user experience.</p>
<h2 id="third-parties"><a class="anchor" href="#third-parties">Third Parties</a></h2>
<p class="centered"><a href="https://almanac.httparchive.org/en/2022/third-parties#performance-impact"><img src="https://imkev.dev/media/blog/web-almanac-2022-third-parties-blocking-main-thread.png" alt="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%." title="Third parties blocking the main thread"></a></p>
<p class="caption">Third parties blocking the main thread</p>
<p>Looking at the performance impact of third parties, YouTube blocks the main thread on <strong>90%</strong> of mobile websites tested, with a median main thread blocking time of <strong>1,721ms</strong>. On the other hand, third parties do a good job at minifying and compressing resources, with <strong>88.4%</strong> of scripts and <strong>95.9%</strong> of CSS compressed with either GZip or Brotli.</p>
<p>All data and queries are publicly available and feedback is highly encouraged.</p>
<p>Thank you and have a good one!</p>
]]></content:encoded>
            <category>Authoring</category>
            <category>Web Almanac</category>
            <category>JavaScript</category>
            <category>Interop</category>
            <category>Third Parties</category>
        </item>
        <item>
            <title><![CDATA[Google IO Extended - Malta]]></title>
            <link>https://imkev.dev/google-io-extended-malta</link>
            <guid>https://imkev.dev/google-io-extended-malta</guid>
            <pubDate>Wed, 07 Sep 2022 12:00:00 GMT</pubDate>
            <description><![CDATA[On June 21st I was lucky enough to speak at Google IO Extended - Malta about Interaction to Next Paint (INP).]]></description>
            <content:encoded><![CDATA[<p>On June 21st I was lucky enough to speak at <a href="https://gdg.community.dev/events/details/google-gdg-malta-presents-google-io-extended-malta/">Google IO Extended - Malta</a>. 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.</p>
<p>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 <a href="mailto:hello@imkev.dev?subject=Request%20for%20Speaker">get in touch</a>.</p>
<p>I am also available to conduct company workshops or presentations. The content will be made specifically for you and your team.</p>
<ul>
<li><a href="https://noti.st/kevinfarrugia/prak9E">Slides</a></li>
<li><a href="https://www.youtube.com/watch?v=6SoO92Yiff4">Video</a> *</li>
</ul>
<p><em>* The event wasn’t recorded, so I recorded a remote session from home.</em></p>
]]></content:encoded>
            <category>Web performance</category>
            <category>Speaking</category>
            <enclosure url="https://imkev.dev/media/blog/io-extended-malta.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Interaction to Next Paint]]></title>
            <link>https://imkev.dev/inp</link>
            <guid>https://imkev.dev/inp</guid>
            <pubDate>Thu, 16 Jun 2022 12:00:00 GMT</pubDate>
            <description><![CDATA[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 might replace First Input Delay (FID) as a Core Web Vital in the future.]]></description>
            <content:encoded><![CDATA[<p><em>Updated: Friday Feb 2 2024</em></p>
<p>In May 2022, Google added Time to First Byte (TTFB) and <a href="https://web.dev/inp/">Interaction to Next Paint</a> (INP) to their CrUX report. INP measures a website’s responsiveness and <a href="https://web.dev/blog/inp-cwv-march-12">will replace First Input Delay (FID) as a Core Web Vital</a> on March 12th, 2024.</p>
<p>Let’s take a look at what Interaction to Next Paint is and how can you prepare your website to have a good INP.</p>
<h2 id="first-input-delay"><a class="anchor" href="#first-input-delay">First Input Delay</a></h2>
<p>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 <em>first</em> interaction on a page.</p>
<p>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.</p>
<p><video height="auto" autoplay muted loop style="display:block;margin:1em auto;max-width:100%"> <source src="/media/blog/responsiveness.mp4" type="video/mp4"> </video></p>
<p class="caption">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.</p>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/good-fid.png" alt="The percentage of origins with good FID experiences, less than or equal to 100 ms" title="The percentage of origins with good FID experiences, less than or equal to 100 ms"></p>
<p class="caption">The number of websites achieving a <a href="https://httparchive.org/reports/chrome-ux-report#cruxFastFid">good FID</a> score has increased from 72.1% of origins in 2018 to 92.5% of origins in May 2022. FID on desktop was always &gt; 98%.</p>
<h2 id="interaction-to-next-paint"><a class="anchor" href="#interaction-to-next-paint">Interaction to Next Paint</a></h2>
<p>INP aims to build on FID but considers the responsiveness of <em>all</em> user inputs. It also captures the entire duration of the interaction, until the next frame is painted onto the screen. Check out this <a href="https://inp-demo.glitch.me/">demo</a> to get a feel of how frustrating a high INP could be.</p>
<h3 id="what-is-an-interaction"><a class="anchor" href="#what-is-an-interaction">What is an interaction?</a></h3>
<p>An <a href="https://web.dev/inp/#what's-in-an-interaction">interaction</a> is one of the following:</p>
<ul>
<li>Clicking or tapping on an interactive element, such as a button or checkbox.</li>
<li>Pressing a key, including text input fields.</li>
</ul>
<p>It does not include hover or scroll events.</p>
<h3 id="how-is-an-interactions-duration-measured"><a class="anchor" href="#how-is-an-interactions-duration-measured">How is an interaction’s duration measured?</a></h3>
<p class="centered"><img src="https://imkev.dev/media/blog/what-is-an-interaction.svg" alt="Breakdown of an interaction" title="Breakdown of an interaction"></p>
<p class="caption">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: <a href="https://web.dev/inp/">https://web.dev/inp/</a></p>
<p>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.</p>
<h3 id="what-is-a-good-inp"><a class="anchor" href="#what-is-a-good-inp">What is a good INP?</a></h3>
<p>In most cases, INP measures the <em>worst</em> 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.</p>
<h2 id="why-is-inp-important"><a class="anchor" href="#why-is-inp-important">Why is INP important?</a></h2>
<p>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.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/js-long-tasks-vs-conversion-rate.png" alt="Correlation between JS Long Tasks and Conversion Rate" title="Correlation between JS Long Tasks and Conversion Rate"></p>
<p class="caption">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.</p>
<p>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 &lt; 500ms JS long tasks is 38.5%, while users that experience &gt; 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!</p>
<p>And with INP 2x <a href="https://colab.research.google.com/drive/12lJmAABgyVjaUbmWvrbzj9BkkTxw6ay2?usp=sharing">more correlated</a> to Total Blocking Time (TBT) than FID, we expect to see a similar negative correlation between INP and the conversion rate.</p>
<h2 id="how-do-you-optimize-inp"><a class="anchor" href="#how-do-you-optimize-inp">How do you optimize INP?</a></h2>
<p>As INP is correlated well with TBT, you could imply that reducing TBT will reduce your INP.</p>
<ul>
<li><a href="https://www.debugbear.com/docs/performance/minimize-main-thread-work">Minimize main-thread work</a>.</li>
<li>Optimize your JavaScript bundles by <a href="https://web.dev/reduce-javascript-payloads-with-code-splitting/">code-splitting</a>.</li>
<li>If you are working on a React app, you should minimize hydration and <a href="https://imkev.dev/optimizing-rerenders">optimize your rerenders</a>.</li>
<li>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.</li>
</ul>
<h2 id="tooling"><a class="anchor" href="#tooling">Tooling</a></h2>
<p>The best way to measure INP is to collect metrics from real users. If your website meets the criteria to be included in the <a href="https://developers.google.com/web/tools/chrome-user-experience-report">CrUX</a> dataset, then you can use the CrUX API’s <code>experimental_interaction_to_next_paint</code> field or via BigQuery.</p>
<p class="centered"><img src="https://imkev.dev/media/blog/page-speed-insights-inp.png" alt="PageSpeed Insights" title="PageSpeed Insights"></p>
<p class="caption">INP is now available on PageSpeed Insights.</p>
<p>You may view INP data using <a href="https://pagespeed.web.dev/">PageSpeed Insights</a> or <a href="https://treo.sh">Treo</a>; however, these still rely on the CrUX dataset.</p>
<p>You can measure the INP metric yourself by using the <a href="https://github.com/GoogleChrome/web-vitals/tree/next"><code>web-vitals</code> library</a> and sending this data to Google Analytics or another analytics endpoint.</p>
<pre><code class="language-js">let maxDuration = 0;

new PerformanceObserver(list =&gt; {
  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 &gt; 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
});
</code></pre>
<p>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.</p>
<h2 id="inp-in-the-wild"><a class="anchor" href="#inp-in-the-wild">INP in the wild</a></h2>
<p>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.</p>
<table class="table">
<thead>
<tr>
<th>Technology</th>
<th>Origins</th>
<th>Good INP</th>
</tr>
</thead>
<tbody>
<tr>
<td>ALL</td>
<td>6,754,899</td>
<td>55%</td>
</tr>
<tr>
<td>jQuery</td>
<td>5,591,806</td>
<td>57%</td>
</tr>
<tr>
<td>AngularJS</td>
<td>85,049</td>
<td>46%</td>
</tr>
<tr>
<td>Vue.js</td>
<td>211,468</td>
<td>42%</td>
</tr>
<tr>
<td>React</td>
<td>582,000</td>
<td>35%</td>
</tr>
<tr>
<td>Preact</td>
<td>183,771</td>
<td>35%</td>
</tr>
<tr>
<td>Svelte</td>
<td>8,062</td>
<td>33%</td>
</tr>
</tbody>
</table>
<p class="caption">CWV technology report - INP data for May 2022</p>
<p>The above table is extracted from the HTTP Archive as of May 2022, using the <a href="https://cwvtech.report">CWV Technology Report</a>. Most modern front-end frameworks do poorly in INP, which isn’t surprising considering how much heavy lifting is being done in JavaScript.</p>
<p>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.</p>
]]></content:encoded>
            <category>Web performance</category>
            <category>Core Web Vitals</category>
            <enclosure url="https://imkev.dev/media/blog/js-long-tasks-vs-conversion-rate.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>