<?xml-stylesheet href="/pretty-feed-v2.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Trys Mudford's Blog</title>
    <link>https://www.trysmudford.com/tags/performance/</link>
    <description>Posts, thoughts, links and photos from Trys</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 01 Oct 2024 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.trysmudford.com/blog/index.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title>I wasted a day on CSS selector performance to make a website load 2ms faster</title>
      <link>https://www.trysmudford.com/blog/i-spent-a-day-making-the-website-go-2ms-faster/</link>
      <pubDate>Tue, 01 Oct 2024 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/i-spent-a-day-making-the-website-go-2ms-faster/</guid>
      <description><![CDATA[
<p>I&rsquo;ve been doing some performance tinkering at work. It&rsquo;s written in <a href="https://nextjs.org/">Next.js</a> and employs judicious use of <a href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">Server Components</a> to minimise client-side JS. I&rsquo;m really proud of it. It scores ~90 on Lighthouse mobile, which for Next.js isn&rsquo;t too bad. Sure, it pains me to see the tiny amount of client-side interaction resulting in such a large bundle, but it&rsquo;s the price we pay for developer convenience, apparently. But I digress.</p>
<p>I was showing some traces to a colleague, and we were looking intently at the considerable gap between the CSS being downloaded, and the website becoming visible.</p>
<p><img src="/images/blog/waterfall.png" alt="A trace from WebPageTest showing the browser main thread locking up before rendering"></p>
<p>My hunch was the bundle of <code>&lt;script&gt;</code>s than Next generates were being parsed at the point of CSS rendering, causing the main thread to choke. Incredibly, you <em>still</em> can&rsquo;t swap <code>async</code> to <code>defer</code> when using the the App Router (despite it being possible with their previous router), which would almost certainly close this gap. But I digress.</p>
<p>He found an option in the &ldquo;Performance&rdquo; tab in dev tools called &ldquo;Enable CSS selector stats (slow)&rdquo;, which analyses every CSS selector and pops them in a table.</p>
<p><img src="/images/blog/selectors.png" alt="The Selector Stats table in Chrome Dev Tools, showing the most &lsquo;expensive&rsquo; selectors"></p>
<p>In addition, the trace showed an alarming blob of purple where the main thread locked up under the label of &ldquo;Recalculate style&rdquo;. 270ms of delay just to calculate styles.</p>
<p>Goodness, I thought, that&rsquo;s some tasty low hanging fruit.</p>
<p>Now, I know in my heart that CSS selector performance isn&rsquo;t something to worry about. I&rsquo;ve heard it again and again that browsers are incredibly efficient at handling CSS selectors these days, and yet the graph didn&rsquo;t (appear to) lie; 270ms to recalculate styles on every DOM change and page load. Against my better judgement, on a day where I didn&rsquo;t have a tonne of other work to do, I felt I had to go deeper.</p>
<p>I was particularly drawn to the column &ldquo;Match attempts&rdquo; compared to &ldquo;Match count&rdquo;. Some selectors were attempting to match against <em>hundreds</em> of DOM nodes, but still end up not matching many/any elements. This fruit was looking tastier by the second.</p>
<p>The common culprits were any selector ending in <code>*, :last-child, :first-child, :nth-child</code>, which included the infamous <a href="https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/">owl selector</a>, being used for <a href="https://piccalil.li/blog/my-favourite-3-lines-of-css/">flow spacing</a> amongst other things.</p>
<p>The penny suddenly dropped onto a piece of knowledge I&rsquo;d squirrelled away years ago. Selectors are read from right to left in CSS. This form of <a href="https://en.wikipedia.org/wiki/Bottom-up_parsing">bottom-up parsing</a> is more efficient for the browser when matching to DOM nodes. However, you can begin to see why a selector like <code>.parent &gt; * + *</code> is flagged as one of the more inefficient ones. Read backwards, the browser sees the selector as follows:</p>
<ol>
<li><code>*</code> - this matches <em>all</em> elements</li>
<li><code>* + *</code> - this narrows it to all elements that follow another (so almost all elements)</li>
<li><code>.parent &gt; * + *</code> - finally, this narrows it down to any direct children of <code>.parent</code> that follow another child</li>
</ol>
<p>So this selector <em>attempts</em> to match on <em>every</em> DOM node, but might only actually match five elements on the page.</p>
<p>Now, at this point, I should&rsquo;ve seen the column marked &lsquo;Elapsed (ms)&rsquo; with values such as <code>0.013</code> and thought, hmm, that doesn&rsquo;t seem very long, nor could all the selectors <em>really</em> add up to 270ms. But by now, the low hanging fruit looked far too tasty, and I had a hankering for some refactoring.</p>
<p>I took the table of selectors and calculated which selectors had the greatest difference between match attempts and elements found, and began working through those. A few were fed through our design system, but many came from my codebase.</p>
<p>Through much inversion of control and additional classes/data attributes, I updated almost all instances of <code>:last-child</code> and <code>*</code>. I made a release to our design system fixing the oh-so-inefficient selectors and marvelled at the improvement.</p>
<blockquote>
<p>Recalculate style /
Before: 270ms /
After: 40ms</p>
</blockquote>
<p>For a few minutes, I felt like Indiana Jones staring at the holy grail. 230ms of savings from some CSS selector changes. This felt <em>significant</em>. And yet, the niggling phrase of &ldquo;CSS selector efficiency is not something to worry about in 2024&rdquo; kept ringing through my head.</p>
<p>I ran the before/after through <a href="https://webpagetest.org/">WebPageTest</a> and to my surprise (and lack of surprise), nothing significant had changed.</p>
<p>It was at this point that the second penny dropped and I realised I&rsquo;d been duped by a graphing misunderstanding and my own enthusiasm for improving frontend performance. When clicking &ldquo;Enable CSS selector stats (slow)&rdquo;, I assumed it took longer to record the rendering process, but would provide the results as if they were not being profiled. I then realised that the graph was showing a big purple blob <em>because</em> I had the checkbox enabled. Suffice to say, I felt like a right doughnut.</p>
<p>I ran it all again without that option enabled and here are the fruits of my labour:</p>
<blockquote>
<p>Recalculate style /
Before: 11.95ms /
After: 10.23ms</p>
</blockquote>
<p>Whoop-de-doo</p>
<p>But if, somehow, you&rsquo;re browsing this site <em>with</em> CSS selector stats enabled, and find it to be a snappy experience, you&rsquo;re welcome.</p>
]]>
      </description>
    </item>
    
    <item>
      <title>Using &#39;use&#39; to stream deferred content</title>
      <link>https://www.trysmudford.com/blog/nextjs-use/</link>
      <pubDate>Fri, 30 Aug 2024 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/nextjs-use/</guid>
      <description><![CDATA[
<p>In the constant bid to improve page performance, I discovered <code>use</code>, an API recently shipped by React, and made available in Next.js. You&rsquo;d be forgiven for not immediately understanding its function, given the oh-so-descriptive name.</p>
<p><a href="https://react.dev/reference/react/use">use</a> lets you read the value of a promise (or context if you&rsquo;re that way inclined). It feels like a bit of syntactic sugar, to avoid writing <code>.then()</code> or <code>await</code> within your component, but I&rsquo;m sure there&rsquo;s more to it than that.</p>
<h2 id="when-is-use-useful">When is &lsquo;use&rsquo; useful?</h2>
<p><img src="/images/blog/use-carousel.jpg" alt=""></p>
<p>Take this carousel. Pretty standard stuff at the end of a product page. Definitely shouldn&rsquo;t be render-blocking, and a prime candidate for deferred loading. We could move the data request into a client-side handler and call <code>useEffect</code> to fetch the data on the client, but finding ways to avoid <code>useEffect</code> becomes a source of personal pride once you&rsquo;ve worked on a React project for any length of time. This approach has to wait for the whole page to hydrate before it can get to work loading the data.</p>
<p>Instead, we can still begin fetching the data on the server, but combine <code>&lt;Suspense&gt;</code> and <code>use</code> to handle the loading state before resolving it on the client.</p>
<h2 id="what-does-use-look-like">What does &lsquo;use&rsquo; look like?</h2>
<p>Before:</p>
<div class="highlight"><pre class="chroma"><code class="language-tsx" data-lang="tsx"><span class="kr">export</span> <span class="k">default</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">Page() {</span>
    <span class="c1">// Wait for the data to load
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">vehicles</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getVehiclesFromDB</span><span class="p">();</span>

    <span class="k">return</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nt">SimilarVehicles</span> <span class="na">vehicles</span><span class="o">=</span><span class="p">{</span><span class="nx">vehicles</span><span class="p">}</span> <span class="p">/&gt;</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="kr">export</span> <span class="kr">const</span> <span class="nx">SimilarVehicles</span>: <span class="kt">React.FC</span><span class="o">&lt;</span><span class="p">{</span>
    <span class="nx">vehicles</span>: <span class="kt">VehiclePreview</span><span class="p">[];</span>
<span class="p">}</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">vehicles</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nt">section</span> <span class="na">className</span><span class="o">=</span><span class="p">{</span><span class="nx">cx</span><span class="p">(</span><span class="nx">styles</span><span class="p">.</span><span class="nx">section</span><span class="p">)}&gt;</span>
            <span class="p">{</span><span class="nx">vehicles</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">vehicle</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
                <span class="p">&lt;</span><span class="nt">VehicleCard</span>
                    <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">vehicle</span><span class="p">.</span><span class="nx">vehicleId</span><span class="p">}</span>
                    <span class="na">vehicle</span><span class="o">=</span><span class="p">{</span><span class="nx">vehicle</span><span class="p">}</span>
                <span class="p">/&gt;</span>
            <span class="p">))}</span>
        <span class="p">&lt;/</span><span class="nt">section</span><span class="p">&gt;</span>
    <span class="p">);</span>
<span class="p">};</span>
</code></pre></div><p>After:</p>
<div class="highlight"><pre class="chroma"><code class="language-tsx" data-lang="tsx"><span class="kr">export</span> <span class="k">default</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">Page() {</span>
    <span class="c1">// Crack on rendering the page
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">vehiclesPromise</span> <span class="o">=</span> <span class="nx">getVehiclesFromDB</span><span class="p">();</span>

    <span class="k">return</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nt">Suspense</span> <span class="na">fallback</span><span class="o">=</span><span class="p">{&lt;</span><span class="nt">LoadingState</span> <span class="p">/&gt;}&gt;</span>
            <span class="p">&lt;</span><span class="nt">SimilarVehicles</span> <span class="na">vehiclesPromise</span><span class="o">=</span><span class="p">{</span><span class="nx">vehiclesPromise</span><span class="p">}</span> <span class="p">/&gt;</span>
        <span class="p">&lt;/</span><span class="nt">Suspense</span><span class="p">&gt;</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="kr">export</span> <span class="kr">const</span> <span class="nx">SimilarVehicles</span>: <span class="kt">React.FC</span><span class="o">&lt;</span><span class="p">{</span>
    <span class="nx">vehiclesPromise</span>: <span class="kt">Promise</span><span class="p">&lt;</span><span class="nt">VehiclePreview</span><span class="err">[]</span><span class="p">&gt;;</span>
<span class="p">}</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">vehiclesPromise</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Resolve the promise
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">vehicles</span> <span class="o">=</span> <span class="nx">use</span><span class="p">(</span><span class="nx">vehiclesPromise</span><span class="p">);</span>

    <span class="k">return</span> <span class="p">(</span>
        <span class="p">&lt;</span><span class="nt">section</span> <span class="na">className</span><span class="o">=</span><span class="p">{</span><span class="nx">cx</span><span class="p">(</span><span class="nx">styles</span><span class="p">.</span><span class="nx">section</span><span class="p">)}&gt;</span>
            <span class="p">{</span><span class="nx">vehicles</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">vehicle</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
                <span class="p">&lt;</span><span class="nt">VehicleCard</span>
                    <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">vehicle</span><span class="p">.</span><span class="nx">vehicleId</span><span class="p">}</span>
                    <span class="na">vehicle</span><span class="o">=</span><span class="p">{</span><span class="nx">vehicle</span><span class="p">}</span>
                <span class="p">/&gt;</span>
            <span class="p">))}</span>
        <span class="p">&lt;/</span><span class="nt">section</span><span class="p">&gt;</span>
    <span class="p">);</span>
<span class="p">};</span>
</code></pre></div><h2 id="how-does-use-work">How does &lsquo;use&rsquo; work?</h2>
<p>By continuing to call the vehicles database from the server, we&rsquo;ve kept our precious server-side secrets safe. But by omitting <code>await</code>, we can crack on with streaming the page to the user much quicker than before.</p>
<p>React pops our fallback component in place, and continues to stream the page in the background until the database call resolves. At that point, the page connection closes and the component inside <code>&lt;Suspense&gt;</code> is rendered. <code>use</code> converts the promise over to our intended data format, and the component renders as expected.</p>
<p>You can even use <code>ErrorBoundary</code> to handle promise failures. Pretty neat.</p>
<h2 id="does-use-work-without-js">Does &lsquo;use&rsquo; work without JS?</h2>
<p>Sadly not. React needs JS at runtime to swap out the loading content with the deferred content. Non-JS users will be left with the loading component for infinity. So this isn&rsquo;t appropriate for core user journey functionality, or spider-friendly content.</p>
]]>
      </description>
    </item>
    
    <item>
      <title>Standing still - a performance tinker</title>
      <link>https://www.trysmudford.com/blog/standing-still/</link>
      <pubDate>Mon, 15 Apr 2024 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/standing-still/</guid>
      <description><![CDATA[
<p>When I launched this site, it hit 100&rsquo;s across the board on Lighthouse. Using a <a href="https://gohugo.io/">static site generator</a> and very little JS did most of the heavy lifting. But when I checked a couple of days ago, things weren&rsquo;t looking so rosy.</p>
<p><img src="/images/blog/lighthouse-before.webp" alt="A screenshot of the Chrome lighthouse report: 63 for performance, 98 for SEO & accessibility, 100 for best practices and PWA"></p>
<p>I&rsquo;ve been standing still while the performance world has moved on. It&rsquo;s exciting - not only is there plenty of room for improvement, it shows the benchmarks are moving in the right direction. We have new image formats, new loading technqiues, heck, even a new <a href="https://en.wikipedia.org/wiki/HTTP/3">HTTP protocol</a> available, so it we should expect better performance than 5 years ago, even if the JS-fuelled reality of the web suggests otherwise.</p>
<h2 id="images">Images</h2>
<h3 id="image-formats">Image formats</h3>
<p>First up, image formats. This is the lowest of the low hanging fruit. WebP and AVIF both launched since I built <a href="/blog/the-fourth-incarnation/">this incarnation</a> of my site. Images account for <code>526 kB / 702 kB</code> sent down the wire to render my home page. <a href="https://squoosh.app/">Squoosh to the rescue</a>. Once all the images are converted, I can update the markup with the <code>&lt;picture&gt;</code> element:</p>
<h4 id="before">Before</h4>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;images/trysmudford.jpg&#34;</span> <span class="na">alt</span><span class="o">=</span><span class="s">&#34;Photo of Trys Mudford, standing, smiling and facing the right&#34;</span> <span class="p">/&gt;</span>
</code></pre></div><h4 id="after">After</h4>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">picture</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">source</span> <span class="na">srcset</span><span class="o">=</span><span class="s">&#34;images/trysmudford.avif&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;image/avif&#34;</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nt">source</span> <span class="na">srcset</span><span class="o">=</span><span class="s">&#34;images/trysmudford.webp&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;image/webp&#34;</span> <span class="p">/&gt;</span>
  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;images/trysmudford.jpg&#34;</span> <span class="na">alt</span><span class="o">=</span><span class="s">&#34;Photo of Trys Mudford, standing, smiling and facing the right&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">picture</span><span class="p">&gt;</span>
</code></pre></div><p>The result? <code>117 kB / 435 kB</code>. A whooping <code>409 kB</code> saved! Turns out, I&rsquo;d previously converted my hero photo to WebP, trying to be a good citizen, but clearly done a bad job of it, as it was four times larger than the compressed JPG it was replacing!</p>
<h3 id="image-loading">Image loading</h3>
<p>Native lazy loading is such a great web platform feature, and effortless to add. Popping <code>loading=&quot;lazy&quot;</code> to any image (below the fold) will defer the image network request until the browser scrolls close enough to the element to need it.</p>
<h3 id="image-width--height">Image width &amp; height</h3>
<p>Cumulative layout shift can be exacerbated by lazy loading images, so we can use the width/height attributes to give the browse a clue as to how large the image <em>will</em> be, before it makes the network request. It can then plot out the space required and reduce expensive shifts to the page layout when the image does load.</p>
<h2 id="accessibility">Accessibility</h2>
<h3 id="heading-order">Heading order</h3>
<p>The only accessibility warning was about heading-level orders. The side projects on the site used H3&rsquo;s where H2&rsquo;s would&rsquo;ve been more appropriate. A nice quick win.</p>
<h3 id="tap-target-size">Tap target size</h3>
<p>The final warning was about tap target sizes, namely for the category links that sit next to each blog post. Given the tricky rotation applied to them, I feared this might be a bit fiddly. I was wrong, a quick addition of <code>padding: var(--space-2xs);</code> handled it rather nicely.</p>
<h2 id="result">Result</h2>
<p>And with that, we&rsquo;re back to 100&rsquo;s across the baord. I&rsquo;ve shaved nearly half a megabyte off the page size and improved the accessibility along the way. Not bad for an evening of tinkering.</p>
<p><img src="/images/blog/lighthouse-after.webp" alt="A screenshot of the Chrome lighthouse report: all 100&rsquo;s and some confetti"></p>
]]>
      </description>
    </item>
    
    <item>
      <title>Declarative Tracking</title>
      <link>https://www.trysmudford.com/blog/declarative-tracking/</link>
      <pubDate>Fri, 08 Dec 2023 00:00:00 +0000</pubDate>
      
      <guid>https://www.trysmudford.com/blog/declarative-tracking/</guid>
      <description><![CDATA[
<p>On &lsquo;reactive&rsquo; projects, it&rsquo;s pretty common to attach event tracking to buttons/forms etc using inline <code>onClick</code>/<code>onSubmit</code> handlers. It usually starts with the harmless &ldquo;can we track how many times this button is pressed&rdquo; request, and without thinking, you attach a handler that looks something like this: <code>onClick={() =&gt; ga('event'))</code>. This continues for many months &amp; years until your codebase is littered with these inline tracking functions.</p>
<p>I&rsquo;ve had the opportunity to work on a greenfield project recently and was able to trial an alternative approach. A more declarative approach. I&rsquo;m certainly not the first person to think of this, but I wanted to share my experience. It looks a bit like this:</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">button</span>
    <span class="na">type</span><span class="o">=</span><span class="s">&#34;button&#34;</span>
    <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;cta&#34;</span>
    <span class="na">data-track-category</span><span class="o">=</span><span class="s">&#34;homepage&#34;</span>
    <span class="na">data-track-name</span><span class="o">=</span><span class="s">&#34;heroCta&#34;</span>
<span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">form</span> <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;submit&#34;</span> <span class="na">data-track-category</span><span class="o">=</span><span class="s">&#34;createAccount&#34;</span><span class="p">&gt;</span>

<span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;...&#34;</span> <span class="na">data-track</span><span class="o">=</span><span class="s">&#34;click&#34;</span><span class="p">&gt;</span>
</code></pre></div><p>Any anchor, form, or button can be marked up with <code>data-</code> attributes that describe the event. Rather than attach a handler to each event, we register one global <code>click</code> and <code>submit</code> event handler on the <code>document</code>. Thanks to event bubbling and <code>event.target.closest</code>, we can easily filter out elements that don&rsquo;t have the <code>data-track</code> attribute.</p>
<h2 id="benefits">Benefits</h2>
<ul>
<li>This is a substantially more efficient pattern to attaching events to each element, particularly in a re-render heavy framework like React</li>
<li>Abstracting this functionality allows us to declare less per element. For example, we can read <code>event.target.textContent</code>, rather than re-supply the label on every call-to-action button. We can automatically pull in the <code>href</code> on an anchor once, rather than duplicate the URL in an inline function</li>
<li>There&rsquo;s less code to write per component</li>
<li>We can add new tracking providers without touching every component</li>
<li>Common values can be set globally, rather than per component. For example, we may wish to pass the current page URL for all events</li>
<li>This all started in a Next.js project. I&rsquo;d painstakingly constructed my server/client components to minimise the number of client components. And that would&rsquo;ve been entirely obliterated with inline tracking functions, that have to exist in a <code>'use client'</code> file. This approach allowed me to keep as many components as pure server components</li>
</ul>
<h2 id="drawbacks">Drawbacks</h2>
<ul>
<li>It&rsquo;s extra HTML to send over the wire - sending markup for something that is entirely client-side isn&rsquo;t ideal</li>
<li>Everything that&rsquo;s tracked is easier to see in dev-tools - it&rsquo;s not as if inline functions are hidden, but they&rsquo;re certainly less obvious. This could be a positive; if you&rsquo;re not comfortable with someone seeing what&rsquo;s being tracked, then maybe it shouldn&rsquo;t be tracked? The tracking sunlight test</li>
<li>Non-primitives can&rsquo;t easily be tracked without stringifying JSON</li>
</ul>
<h2 id="abstracting-the-tracking">Abstracting the tracking</h2>
<p>Rather than calling, say, Google Analytics directly in the global handler, I&rsquo;ve taken to dispatching a <code>CustomEvent</code> in a consistent format that can be listened to by any number of handlers. For the few times you may need to track something outside of the global handler, you can dispatch a <code>CustomEvent</code> with the same format and it&rsquo;ll be fed to all the handlers.</p>
<h2 id="implementation">Implementation</h2>
<p>This is a rough implementation of the concept - I can&rsquo;t guarantee perfection but it&rsquo;s broadly what I&rsquo;ve been using:</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kr">const</span> <span class="nx">TRACKING_EVENT</span> <span class="o">=</span> <span class="s1">&#39;namespaced:tracking&#39;</span><span class="p">;</span>

<span class="c1">// Fire events to all tracking providers
</span><span class="c1"></span><span class="kr">const</span> <span class="nx">trackEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventAction</span><span class="p">,</span> <span class="nx">trackingDetail</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span>
        <span class="k">new</span> <span class="nx">CustomEvent</span><span class="p">(</span><span class="nx">TRACKING_EVENT</span><span class="p">,</span> <span class="p">{</span>
            <span class="nx">detail</span><span class="o">:</span> <span class="p">{</span>
                <span class="nx">eventAction</span><span class="p">,</span>
                <span class="nx">trackingDetail</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">}),</span>
    <span class="p">);</span>
<span class="p">};</span>

<span class="kr">const</span> <span class="nx">globalTrackingHandler</span> <span class="o">=</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">event</span><span class="o">:</span> <span class="nx">Event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Detect if we&#39;re on a trackable element
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span> <span class="nx">as</span> <span class="nx">HTMLElement</span><span class="p">).</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;[data-track]&#39;</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">target</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="c1">// Prevent click events from firing on trackable forms
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">action</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">track</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">action</span> <span class="o">===</span> <span class="s1">&#39;submit&#39;</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="s1">&#39;click&#39;</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="c1">// Extract data from the trackable element
</span><span class="c1"></span>    <span class="kr">const</span> <span class="nx">category</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-category&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">label</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-label&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-name&#39;</span><span class="p">];</span>
    <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">dataset</span><span class="p">[</span><span class="s1">&#39;track-name&#39;</span><span class="p">]</span> <span class="o">??</span> <span class="nx">target</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;href&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">;</span>

    <span class="c1">// Pass events to the global tracking helper
</span><span class="c1"></span>    <span class="k">switch</span> <span class="p">(</span><span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="s1">&#39;click&#39;</span><span class="o">:</span> <span class="p">{</span>
            <span class="nx">trackEvent</span><span class="p">(</span><span class="nx">action</span><span class="p">,</span> <span class="p">{</span>
                <span class="nx">category</span><span class="p">,</span>
                <span class="nx">label</span><span class="p">,</span>
                <span class="nx">name</span><span class="p">,</span>
                <span class="nx">url</span><span class="p">,</span>
            <span class="p">});</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">case</span> <span class="s1">&#39;submit&#39;</span><span class="o">:</span> <span class="p">{</span>
            <span class="kr">const</span> <span class="nx">formLabel</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;[type=&#34;submit&#34;]&#39;</span><span class="p">)</span><span class="o">?</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">??</span> <span class="nx">label</span><span class="p">;</span>
            <span class="kr">const</span> <span class="nx">formUrl</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;action&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="nx">url</span><span class="p">;</span>
            <span class="nx">trackEvent</span><span class="p">(</span><span class="s1">&#39;cta&#39;</span><span class="p">,</span> <span class="p">{</span>
                <span class="nx">category</span><span class="p">,</span>
                <span class="nx">label</span><span class="o">:</span> <span class="nx">formLabel</span><span class="p">,</span>
                <span class="nx">name</span><span class="p">,</span>
                <span class="nx">url</span><span class="o">:</span> <span class="nx">formUrl</span><span class="p">,</span>
            <span class="p">});</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">default</span><span class="o">:</span>
            <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">globalTrackingHandler</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;submit&#39;</span><span class="p">,</span> <span class="nx">globalTrackingHandler</span><span class="p">);</span>

<span class="kr">const</span> <span class="nx">specificTrackingHandler</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="kr">const</span> <span class="p">{</span> <span class="nx">detail</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">detail</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="kr">const</span> <span class="p">{</span> <span class="nx">eventAction</span><span class="p">,</span> <span class="nx">trackingDetail</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">detail</span><span class="p">;</span>

    <span class="k">switch</span> <span class="p">(</span><span class="nx">eventAction</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="s1">&#39;click&#39;</span><span class="o">:</span>
            <span class="nx">trackClick</span><span class="p">(</span><span class="nx">trackingDetail</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">case</span> <span class="s1">&#39;cta&#39;</span><span class="o">:</span>
            <span class="nx">trackCta</span><span class="p">(</span><span class="nx">trackingDetail</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="k">default</span><span class="o">:</span>
            <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">TRACKING_EVENT</span><span class="p">,</span> <span class="nx">specificTrackingHandler</span><span class="p">);</span>
</code></pre></div>]]>
      </description>
    </item>
    
  </channel>
</rss>