Simon HearneSimon Hearne's blog2022-03-22T10:00:00Zhttps://simonhearne.com/Simon Hearnesimon@hearne.meOptimising Core Web Vitals on SPAs2022-03-22T10:00:00Zhttps://simonhearne.com/2022/core-web-vitals-on-spas/<p>Approximately 40% of my clients have single-page applications (SPAs), and approximately 100% of my clients care about core web vitals - the key performance metrics which impact Google search ranking.</p>
<p>Consistently measuring performance of every website and framework is an impossible task, so the core web vitals (largest contentful paint, first input delay and cumulative layout shift) are lowest common denominator metrics. This means that they have been designed to work almost everywhere, but one of the most important aspects of SPAs is missing: route changes (aka in-app navigations).</p>
<p>The core web vital metrics were designed to be better proxies of user experience, but we need to be mindful of how they apply to SPAs. There are more details on the <strong>core</strong> web vitals in this post, but in summary:</p>
<ul>
<li><strong>LCP</strong>, <strong>FID</strong>, FCP, TTFB, TBT and TTI - only measured on session landing pages</li>
<li><strong>CLS</strong> (& Responsiveness) - measured throughout a session, the highest / worst value is collected and <em>attributed to the landing page URL</em></li>
</ul>
<h2 id="largest-contentful-paint" tabindex="-1">Largest Contentful Paint <a class="direct-link" href="https://simonhearne.com/2022/core-web-vitals-on-spas/#largest-contentful-paint" aria-hidden="true">#</a></h2>
<p>LCP is my favourite web performance metric; it captures a simple event in a web experience — when the biggest thing is painted to screen — and envelopes all sorts of important elements of the page load such as time to first byte, document size, blocking JS and CSS.</p>
<p>Only the first landing page in a session will generate LCP values on SPA sites. The <a href="https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint">specification</a> calls for LCP capture to start at a navigation event and stop <em>after the first user interaction</em>. Route-changes / in-app navigations occur after an interaction and thus are not captured by Chrome for the CrUX dataset.</p>
<p>LCP is captured for every individual page load with MPAs / traditional web apps. This means that mid-session visitors will likely generate fast LCP values due to having a warm cache and connection - and if your average session length is greater than 4.0 this will probably result in a faster 75th percentile value. As LCP is only captured on landing pages for SPAs, the results are biased towards the worst case experience of an empty cache. This potentially means that for two identical sites, the SPA will report worse LCP values.</p>
<p>Whilst mid-session experiences are certainly important for UX, we should focus primarily on landing pages to optimise for Google's core web vitals. This means that we should do everything to reduce the critical path to LCP on a first-view. These first experiences are also crucial to improve user experience and reduce bounce rates!</p>
<p>Unfortunately most SPA frameworks don't make this easy out of the box. E.g. if your product image is defined in data then the image element won't even by injected into the DOM until the application JavaScript has downloaded, parsed and executed. This can lead to late asset discovery and correspondingly poor LCP values.</p>
<p>Server-side rendering (SSR) is key here: first-views should deliver the page as HTML and allow the browser to apply its optimisations to download content in the correct order. Just ensure that hydration doesn't cause reflow!</p>
<p>SSR alone won't save us though. The example waterfall chart below shows an SSRd SPA landing page with some common performance anti-patterns: a very large HTML document and a large amount of preloaded JavaScript assets. Note how these delay the download of the image on line 35, which is the LCP element for this page (LCP is marked as the vertical dashed green line at 5.6s).</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cwvs-spas/lcp_waterfall.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/mjqX39Sp8v-600.avif 600w, https://simonhearne.com/img/mjqX39Sp8v-900.avif 900w, https://simonhearne.com/img/mjqX39Sp8v-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/mjqX39Sp8v-600.webp 600w, https://simonhearne.com/img/mjqX39Sp8v-900.webp 900w, https://simonhearne.com/img/mjqX39Sp8v-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/mjqX39Sp8v-600.jpeg 600w, https://simonhearne.com/img/mjqX39Sp8v-900.jpeg 900w, https://simonhearne.com/img/mjqX39Sp8v-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="annotated web page test waterfall showing large HTML document and 1.5MB of preloaded JS blocking LCP" loading="lazy" decoding="async" src="https://simonhearne.com/img/mjqX39Sp8v-600.jpeg" width="1200" height="1117" /></picture></a><figcaption>Use WebPageTest to generate a landing page waterfall and trace back from LCP</figcaption></figure>
<p>I've previously written some advice on <a href="https://simonhearne.com/2020/core-web-vitals/#largest-contentful-paint-lcp">optimising for LCP</a> which is a good place to start. Note that LCP varies by viewport (e.g. a hero image on mobile might be smaller than your heading text element on desktop). Another LCP nuance is that cookie consent banners may be your LCP element for first-time visitors, so make sure to test and optimise for multiple user states! <a href="https://twitter.com/csswizardry">Harry Roberts</a> has written up a great summary of <a href="https://csswizardry.com/2022/03/optimising-largest-contentful-paint/">the gotchas with LCP</a>.</p>
<h2 id="cumulative-layout-shift" tabindex="-1">Cumulative Layout Shift <a class="direct-link" href="https://simonhearne.com/2022/core-web-vitals-on-spas/#cumulative-layout-shift" aria-hidden="true">#</a></h2>
<p>CLS is my second-favourite web performance metric; it captures the previously intangible feeling of a page being unstable — how much content moves around when you aren't expecting it.</p>
<p>Unlike LCP, CLS can be captured throughout a user's session on a SPA. Unexpected layout shifts are <a href="https://web.dev/evolving-cls/#why-a-session-window">accumulated in clusters</a>: if three shifts of 0.1 occur within one second then your CLS score would be 0.3. CLS clusters close after one second of inactivity or after a maximum of five seconds; importantly this process occurs throughout a user's session on your SPA and the <em>largest</em> CLS score by the end of the session <em>is attributed to the session landing page</em>.</p>
<p>This attribution issue means that identifying causes of layout shifts can be difficult. If you see high CLS reports in Google Search Console they will be logged against the session landing page, but could have occurred at any point in the user's session.</p>
<p>CLS has some further nuances: layout shifts are only <strong>unexpected</strong> (and thus count towards CLS) if they are not preceded by a discrete user interaction (click, tap or keypress) within 500ms. This can mean that users on slow network connections report a higher CLS: if for example your search API normally responds in under 500ms from a button press, the subsequent layout shifts are <strong>expected</strong>. In scenarios where the search API takes longer than 500ms, the layout shifts caused by rendering the results will exceed the 500ms threshold and thus become <strong>unexpected</strong>. I call these un-unexpected layout shifts, and on SPAs they can have a significant impact when route changes take longer than half a second.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cwvs-spas/unexpected-shift.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/znfY86js9U-600.avif 600w, https://simonhearne.com/img/znfY86js9U-900.avif 900w, https://simonhearne.com/img/znfY86js9U-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/znfY86js9U-600.webp 600w, https://simonhearne.com/img/znfY86js9U-900.webp 900w, https://simonhearne.com/img/znfY86js9U-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/znfY86js9U-600.jpeg 600w, https://simonhearne.com/img/znfY86js9U-900.jpeg 900w, https://simonhearne.com/img/znfY86js9U-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome developer tools showing an unexpected layout shift during a route change" loading="lazy" decoding="async" src="https://simonhearne.com/img/znfY86js9U-600.jpeg" width="1200" height="980" /></picture></a><figcaption>A route change here took longer than 500ms, resulting in an 'unexpected' layout shift</figcaption></figure>
<p>Another nuance in the <em>expectedness</em> of layout shifts is the user input types: scrolls do not (currently) count as a discrete user input, so any layout shifts on scroll will count towards CLS scores - thus it is important to have <code>width</code> and <code>height</code> attributes on lazy-loaded images! Also beware of lazy-loaded components, Simon Wicki has a <a href="https://wicki.io/posts/2022-03-cls-with-lazy-loading-components/">great blog post</a> on this topic.</p>
<p>These nuances mean that optimising for CLS on your SPA is more complex than it may seem at first. I suggest three separate areas of focus:</p>
<ol>
<li>Reduce frequency and size of layout shifts on first-view (easy)</li>
<li>Remove layout shifts that occur on scroll (moderate)</li>
<li>Investigate potential un-unexpected layout shifts (tricky)</li>
</ol>
<p>This process (especially for 2. and 3.) is manual and time consuming. Where possible, collect web vitals metrics from your route changes using the <a href="https://github.com/GoogleChrome/web-vitals">web-vitals</a> JS library or a RUM solution like <a href="https://www.akamai.com/products/mpulse-real-user-monitoring">Akamai mPulse</a> to help identify the layout shifts that your users are experiencing, attributed to the correct URL.</p>
<h2 id="first-input-delay" tabindex="-1">First Input Delay <a class="direct-link" href="https://simonhearne.com/2022/core-web-vitals-on-spas/#first-input-delay" aria-hidden="true">#</a></h2>
<p><a href="https://web.dev/fid/">FID</a> is one of my least favourite web performance metrics — I have yet to work with a client where their FID numbers showed anything of interest. That said, FID is theoretically a bigger issue on SPAs than MPAs.</p>
<p>FID measures the delay between the first user interaction with your page and the browser being able to respond to it. Basically it represents how likely it is that a user is unlucky and tries to interact with the page whilst a long task is consuming the browser main thread.</p>
<p>The reason I say that FID is more important to SPAs than MPAs is simply due to the reliance on JavaScript for rendering or hydrating pages. Whether your application is client-side rendered or SSRd, there will be a significant chunk of JavaScript execution during the page load to set the page up. FID values on SPAs are biased towards the worst part of the experience as FID is only captured on the landing page.</p>
<p>Optimising FID itself is nearly impossible: the values vary by the visitor's device and the relative timing of their interaction. Instead, focus on reducing total blocking time (TBT). TBT is effectively the sum of long tasks during a page load: a high value indicates that it is more likely that users will try to interact during a long task and thus have a high FID.</p>
<p>You can theoretically improve FID by discouraging interaction until JavaScript execution has completed. I have seen this in practice on some very heavy SPAs (e.g. interactive gaming & gambling) where a splash screen is shown until the application is completely ready. Whilst this will improve FID, it could well degrade LCP, introduce <a href="https://www.koozai.com/blog/search-marketing/why-splash-pages-are-bad-for-seo/">splash-screen SEO penalties</a> and certainly won't have a positive effect on user experience.</p>
<p>Back to TBT — there are four key ways to reduce the impact of long tasks and positively impact FID:</p>
<ol>
<li>Remove expensive JavaScript (still using <a href="https://momentjs.com/docs/#:~:text=We%20now%20generally%20consider%20Moment%20to%20be%20a%20legacy%20project%20in%20maintenance%20mode">moment.js</a>?!)</li>
<li>Choreograph long tasks to occur when users are less likely to interact</li>
<li>Reduce long tasks by breaking expensive functions into chunks (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback">requestIdleCallback</a> for a future option here)</li>
<li>Remove all synchronous API calls</li>
</ol>
<p>To find long tasks: set CPU and Network throttling then record a performance profile of a page load. Look for long tasks in the profile and tackle them from longest to shortest. The example below is an innocuous task which depends on a synchronous XHR - it may not even appear as a long task without network throttling!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cwvs-spas/long-task.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/xvWYxy6TRX-600.avif 600w, https://simonhearne.com/img/xvWYxy6TRX-900.avif 900w, https://simonhearne.com/img/xvWYxy6TRX-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/xvWYxy6TRX-600.webp 600w, https://simonhearne.com/img/xvWYxy6TRX-900.webp 900w, https://simonhearne.com/img/xvWYxy6TRX-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/xvWYxy6TRX-600.jpeg 600w, https://simonhearne.com/img/xvWYxy6TRX-900.jpeg 900w, https://simonhearne.com/img/xvWYxy6TRX-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome timeline showing a three second long task due to a synchronous XHR" loading="lazy" decoding="async" src="https://simonhearne.com/img/xvWYxy6TRX-600.jpeg" width="1200" height="309" /></picture></a><figcaption>Long task due to synchronous XHR discovered using network throttling</figcaption></figure>
<h2 id="bonus-responsiveness" tabindex="-1">Bonus: Responsiveness <a class="direct-link" href="https://simonhearne.com/2022/core-web-vitals-on-spas/#bonus-responsiveness" aria-hidden="true">#</a></h2>
<p><a href="https://web.dev/responsiveness/">Responsiveness</a> is a new web vital being tested by Google. First input delay has received some negative feedback due to being difficult to optimise for and hard to understand. Another issue with FID is that <a href="https://almanac.httparchive.org/en/2021/performance#first-input-delay-fid">almost all sites</a> (100% of desktop and 90% of mobile) pass the FID assessment, meaning it is not actionable for most site owners.</p>
<p>Responsiveness is a potential replacement for FID, and in my clients it often shows a failing result where FID passes. For example a SPA website I'm currently working on:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cwvs-spas/responsiveness-treo.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/jjVLcSU7_w-600.avif 600w, https://simonhearne.com/img/jjVLcSU7_w-900.avif 900w, https://simonhearne.com/img/jjVLcSU7_w-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/jjVLcSU7_w-600.webp 600w, https://simonhearne.com/img/jjVLcSU7_w-900.webp 900w, https://simonhearne.com/img/jjVLcSU7_w-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/jjVLcSU7_w-600.jpeg 600w, https://simonhearne.com/img/jjVLcSU7_w-900.jpeg 900w, https://simonhearne.com/img/jjVLcSU7_w-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of crux data showing 0ms FID but 400ms responsiveness" loading="lazy" decoding="async" src="https://simonhearne.com/img/jjVLcSU7_w-600.jpeg" width="1200" height="430" /></picture></a><figcaption>0ms First Input Delay but 400ms for Responsiveness</figcaption></figure>
<p>The boundaries for good / needs improvement / poor are currently the same as FID: <100ms / <300ms / ≥300ms respectively.</p>
<p>My concern for responsiveness on SPAs is that this new metric captures the <em>full</em> interaction. FID only measures the time the browser takes to register a user input, whilst responsiveness measures the end-to-end interaction latency; this potentially includes the client-side processing to respond to the user's input and paint the next frame on screen. For SPAs this could include the logic to execute a route change which can often be an expensive process. The metric may in the future also consider scroll events (currently ignored for FID) which could reflect negatively on infinite-scrollers or lazy loaded components.</p>
<p>If your responsiveness value is currently high (I love <a href="https://treo.sh/sitespeed/">Treo</a> for quickly seeing CrUX data), this indicates that user interactions on your site may feel slow. The metric itself is still in flux and won't be impacting the page ranking SEO factor for a reasonable time — Google has said that there will be a six month warning before any changes to the core web vitals are pushed live — but it still makes sense to monitor and optimise for responsiveness.</p>
<p>A quick method to determine where a high responsiveness value comes from is to record a profile during an interaction. I use Chrome developer tools for this: load a landing page, then hit record in the performance tab and make an interaction. Look for any delays to rendering the first frame after the interaction and focus your optimisation efforts there. In this case, a click triggers a route change which takes a little over 100ms from the first interaction event to the first frame painted to screen:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cwvs-spas/responsiveness-trace.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/yr6XN82w_L-600.avif 600w, https://simonhearne.com/img/yr6XN82w_L-900.avif 900w, https://simonhearne.com/img/yr6XN82w_L-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/yr6XN82w_L-600.webp 600w, https://simonhearne.com/img/yr6XN82w_L-900.webp 900w, https://simonhearne.com/img/yr6XN82w_L-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/yr6XN82w_L-600.jpeg 600w, https://simonhearne.com/img/yr6XN82w_L-900.jpeg 900w, https://simonhearne.com/img/yr6XN82w_L-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome developer tools showing long task before frame render after user interaction" loading="lazy" decoding="async" src="https://simonhearne.com/img/yr6XN82w_L-600.jpeg" width="1200" height="417" /></picture></a><figcaption>Chrome performance tab shows delays between interaction and next frame paint</figcaption></figure>
<p>To reduce the reported responsiveness value we need to review the long task, the multiple style recalculations and garbage collection event.</p>
<h2 id="in-summary" tabindex="-1">In Summary <a class="direct-link" href="https://simonhearne.com/2022/core-web-vitals-on-spas/#in-summary" aria-hidden="true">#</a></h2>
<p>Core web vitals are useful metrics, and important to optimise for. If you run a SPA focus initially on landing page experiences: these are important not only for the page experience ranking factor but also to reduce bounce rate and improve UX.</p>
<p>Don't be dismayed if you have odd results with cumulative layout shift appearing in Google Search Console. Fixing layout shifts is often easy, as long as you can find them! Record a performance timeline whilst navigating your application in multiple device profiles (at least phone, tablet & desktop) and look for unexpected layout shifts with high scores, I tend to focus on any shift with a score of over 0.05.</p>
<p>If you are still struggling to get your core web vitals into the green, feel free to <a href="https://simonhearne.com/consultancy/">reach out to me</a> for further advice.</p>
Fixing empty responses from Cloudflare Workers Sites2022-02-22T00:00:00Zhttps://simonhearne.com/2022/empty-responses-cloudflare-workers-sites/<p>I'm currently working on a client project using Cloudflare Workers Sites. Mostly development has been a joy, the <code>wrangler</code> CLI is easy to use and production deployment happens in seconds.</p>
<p>There was one strange issue reported by the client which left me looking for answers: sometimes the pages wouldn't render, other times no styles were applied. Occasionally images wouldn't render.</p>
<p>These issues were obviously critical, and I managed to reproduce them only on the production deployment but not with <code>wrangler dev</code>. The cause of the broken pages was empty responses — occasionally a response would appear to be successful (<code>200</code> etc.) but there was no content in the body. I had some custom code messing with headers in the document handler, but nothing that modified the CSS or image responses.</p>
<p>The biggest clue to the cause of the issue is that it never occurred with a hard reload, only normal navigations or soft reloads.</p>
<p>It turns out that in this Workers Sites deployment a <code>last-modified</code> header is added to responses by default (there's no code in <a href="https://github.com/cloudflare/worker-sites-template/blob/master/workers-site/index.js">the bundle</a> which does this). As described in my <a href="https://simonhearne.com/2022/caching-header-best-practices/">blog post on caching</a>, this header allows the browser to emit a conditional GET request with the <code>if-modified-since</code> header. The correct response from the server is a <code>200</code> with the full response if the browser's timestamp is earlier than the server's, or a <code>304</code> empty response if the browser has the latest version of the asset.</p>
<p>It appears that this Workers Sites deployment does not correctly respond with a <code>304</code> — instead it emits an empty <code>200</code> which causes all of the issues we had observed.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/cf_empty_response.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/W20LiW5r1m-600.avif 600w, https://simonhearne.com/img/W20LiW5r1m-900.avif 900w, https://simonhearne.com/img/W20LiW5r1m-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/W20LiW5r1m-600.webp 600w, https://simonhearne.com/img/W20LiW5r1m-900.webp 900w, https://simonhearne.com/img/W20LiW5r1m-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/W20LiW5r1m-600.jpeg 600w, https://simonhearne.com/img/W20LiW5r1m-900.jpeg 900w, https://simonhearne.com/img/W20LiW5r1m-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of developer tools showing a 200 response with an empty body, with last modified response header and if modified since request header" loading="lazy" decoding="async" src="https://simonhearne.com/img/W20LiW5r1m-600.jpeg" width="1200" height="1091" /></picture></a><figcaption>Empty 200 responses when <code>if-modified-since</code> request header set.</figcaption></figure>
<p>I'm not a fan of <code>last-modified</code> as a caching strategy (especially without a <code>cache-control</code> directive) so my quick fix was to delete this response header in the event handler function in <code>index.js</code>:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">handleEvent</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><br /> <span class="token keyword">let</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> page <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getAssetFromKV</span><span class="token punctuation">(</span>event<span class="token punctuation">,</span> options<span class="token punctuation">)</span><br /> <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span>page<span class="token punctuation">.</span>body<span class="token punctuation">,</span> page<span class="token punctuation">)</span><br /><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'X-XSS-Protection'</span><span class="token punctuation">,</span> <span class="token string">'1; mode=block'</span><span class="token punctuation">)</span><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'X-Content-Type-Options'</span><span class="token punctuation">,</span> <span class="token string">'nosniff'</span><span class="token punctuation">)</span><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'X-Frame-Options'</span><span class="token punctuation">,</span> <span class="token string">'DENY'</span><span class="token punctuation">)</span><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'Referrer-Policy'</span><span class="token punctuation">,</span> <span class="token string">'unsafe-url'</span><span class="token punctuation">)</span><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'Feature-Policy'</span><span class="token punctuation">,</span> <span class="token string">'none'</span><span class="token punctuation">)</span><br /><br /> <span class="token comment">// delete the Last-Modified response header</span><br /> response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">'Last-Modified'</span><span class="token punctuation">)</span><br /><br /> <span class="token keyword">return</span> response<br /> <span class="token punctuation">}</span><br /><span class="token operator">...</span></code></pre>
<p>I still need to implement a correct caching strategy, but at least the site works consistently now!</p>
<p>I've reviewed other Workers Sites properties that I manage and the <code>last-modified</code> response header is not set on those. I'm not sure what is different with this property except that it is the most recent new deployment.</p>
Survivorship Bias in Web Performance2022-02-16T10:00:00Zhttps://simonhearne.com/2022/survorship-bias-in-webperf/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2022/survorship-bias-in-webperf/#introduction" aria-hidden="true">#</a></h2>
<p>Way back in 2010, an engineer at YouTube kicked off <a href="https://blog.chriszacharias.com/page-weight-matters">Project Feather</a>: the goal was to reduce the weight of the popular video player page to improve performance. The project was a success: the page weight was reduced from 1,200kB to 98kB and the total number of requests was cut by 90%.</p>
<p>The analytics of Project Feather experiences did not reflect the improvements though: the aggregate page load time actually <strong>increased</strong>. Further investigation revealed that traffic from countries with poor connectivity had dramatically increased — users who previously could not even load the page were now able to watch videos. These new visitors had relatively slow experiences, bringing the aggregate values down.</p>
<p>There are similar stories from a wide range of websites:</p>
<ul>
<li>the bank who decided not to invest in mobile web because customers preferred desktop online banking to mobile (the mobile site was so slow it was barely usable)</li>
<li>the retailer who had no iPad 1 traffic so stopped testing on iPad 1 (the website failed to load on iPad 1)</li>
<li>the retailer who had very low Android traffic and assumed their target demographic were iPhone users (Android experiences were twice as slow as iPhone)</li>
</ul>
<p>There is a term for this issue: <em>Survivorship Bias</em>. If you search for it term you'll likely see a variation of this image:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/survivorship-bias.svg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/gsBYTa3Dzh-600.avif 600w, https://simonhearne.com/img/gsBYTa3Dzh-900.avif 900w, https://simonhearne.com/img/gsBYTa3Dzh-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/gsBYTa3Dzh-600.webp 600w, https://simonhearne.com/img/gsBYTa3Dzh-900.webp 900w, https://simonhearne.com/img/gsBYTa3Dzh-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/gsBYTa3Dzh-600.jpeg 600w, https://simonhearne.com/img/gsBYTa3Dzh-900.jpeg 900w, https://simonhearne.com/img/gsBYTa3Dzh-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="diagram of a plane with red spots to show an aggregate of bullet strikes" loading="lazy" decoding="async" src="https://simonhearne.com/img/gsBYTa3Dzh-600.jpeg" width="1200" height="892" /></picture></a><figcaption>The classic illustration of survivorship bias.</figcaption></figure>
<p>The <a href="https://mcdreeamiemusings.com/blog/2019/4/1/survivorship-bias-how-lessons-from-world-war-two-affect-clinical-research-today">story goes</a> that statistician Abraham Wald used damage logs for aircraft returning from sorties in World War II to identify where reinforcing armour should be applied. The red dots show where bullet holes were found, so the logical assumption would be to reinforce those areas. Wald, however, surmised that the lack of red dots on areas of the aircraft show locations where hits would be critical and result in the aircraft not returning — so the reinforcements should be applied to those areas.</p>
<p>Our decisions are influenced by the data we have, but what about the data we don't have? In the military example above it is clear where the data is missing, but the web is slightly more complex. The users who have the worst experiences are likely to be <a href="https://simonhearne.com/2017/analytics-lie/#phantom-bounces">phantom bounces</a>: they don't appear in your analytics or intelligence tools because they don't hang around long enough for the app to load and analytics to fire.</p>
<h2 id="speed-bias" tabindex="-1">Speed Bias <a class="direct-link" href="https://simonhearne.com/2022/survorship-bias-in-webperf/#speed-bias" aria-hidden="true">#</a></h2>
<p>Google is experimenting with a new web performance metric: <strong>abandonment</strong>. Not to be confused with traditional abandonment metrics like cart abandonment, this is to measure how many users leave a site before it even loads. As part of this research, <a href="https://twitter.com/NicPenaM">Nicolás Peña Moreno</a> in the <a href="https://chromium.googlesource.com/chromium/src/+/master/docs/speed_metrics/README.md">Chrome Speed Metrics</a> team used Chrome data to measure the impact of performance on page abandonment rate — i.e. how many visitors leave a page before it loaded.</p>
<p>The metric used for the performance side of this equation was <a href="https://web.dev/fcp/">First Contentful Paint</a> (FCP), so abandoners were potential visitors who left the site before anything useful was painted on the screen. It's worth noting at this point that it is very difficult for website owners to track this data themselves as it is unlikely that any analytics or tracking code has had a chance to run before FCP (and nor should it!).</p>
<p>I've taken the <a href="https://calendar.perfplanet.com/2020/abandonment/">data that Nicolás shared</a> and created a visualisation below to show the scale of the issue. Unfortunately no sample sizes were included in the results so we can't measure statistical significance at the higher FCP numbers, but the trends speak for themselves:</p>
<div class="vega-chart loading" id="vis1" data-spec="/data/survivorship-bias/chrome-abandonment.json">Loading chart...</div>
<p>On Chrome Android, every second that FCP is delayed results in an additional 2.6 percentage points of abandonment. To clarify with an example: if your FCP was exactly 4s for all Chrome Android users, your analytics likely under-reports these users by 17.3%. So if your analytics shows one million users, there are another 210k users that abandoned before being logged (<code>1,000,000 / ( 1 - 0.173 )</code>). Reducing your FCP by one second would increase your reported visits by 2.9pts or 35,000 (<code>( 1,000,000 / ( 1 - 0.173 ) ) * ( 1 - 0.144 )</code>).</p>
<p>It is clear that poor performance will impact your traffic numbers, and means that thousands of potential visitors are leaving your site without a trace. It is also clear that mobile visitors are less tolerant of slow sites.</p>
<p>There is a secondary issue here: the impact on performance metrics.</p>
<p>It is fair to assume from this data that slower websites don't appear as slow as they are (as the slowest experiences will lead to abandonment) and, as we saw with the YouTube Feather example, making performance improvements may not reflect directly in your data. If we take the values from the chart above and apply them to a typical (log-normal) performance distribution we can predict the impact on reported metrics. A sample data set of 10,000 visits is created and the mobile abandonment statistics applied to see the difference in results, try adjusting the goal mean of the distribution and observe the changes in the table below.</p>
<div class="vega-chart loading" id="vis2" data-spec="/data/survivorship-bias/distribution.json">Loading chart...</div>
<style>
#chartStats th, #chartStats td {
padding: 2px 6px;
}
#chartStats td:first-child {
font-weight: bold;
}
</style>
<table border="1" style="font-size: clamp(0.6rem, 2vw, 1.1rem); min-width:90%;" id="chartStats">
<tr><th>Metric</th><th>Actual Visits</th><th>Excluding Abandons</th><th>Difference</th><th>Relative Difference</th></tr>
<tr><td>Visits</td><td></td><td></td><td></td><td></td></tr>
<tr><td>Median FCP</td><td></td><td></td><td></td><td></td></tr>
<tr><td>75% FCP</td><td></td><td></td><td></td><td></td></tr>
<tr><td>90% FCP</td><td></td><td></td><td></td><td></td></tr>
<tr><td>95% FCP</td><td></td><td></td><td></td><td></td></tr>
</table>
<p>Whilst this data is entirely hypothetical, I hope it helps bring to life the statistics on mobile abandonment shared earlier. Slow sites are more affected by this issue, and so are slow visitors — is your analytics showing a low level of traffic from Android devices because there really are a low number of visitors, or just a high abandonment rate? The difference at high percentiles is worth noting too, your 75th percentile FCP may be reported over a second faster than reality!</p>
<h2 id="actions-to-take" tabindex="-1">Actions to take <a class="direct-link" href="https://simonhearne.com/2022/survorship-bias-in-webperf/#actions-to-take" aria-hidden="true">#</a></h2>
<p>It is extremely difficult to measure the impact of early abandonment as a site owner. It is possible to compare analytics with logs, though, and determine the user agents or regions which have the highest disparity between intent to load a page (from server or CDN logs for documents) and successfully loading the page (analytics or RUM data). This strategy has some issues: some requests in your logs will be for crawlers which don't execute your analytics JavaScript and there will also be some disparity between user agents due to visitors using tracker blockers.</p>
<p>It is possible to beacon data early in a page load in order to better capture visitors who abandon before traditional analytics scripts fire. <a href="https://twitter.com/nicj">Nic Jansma</a> has written up an <a href="https://nicj.net/beaconing-in-practice/#beaconing-reliability">analysis of strategies</a> which shows that beacon capture (on a fast site) can be improved from 86.4% to 92.8% by listening to <code>pagehide</code> and <code>visibilitychange</code> events in addtion to <code>onload</code>. This still requires the JavaScript to execute before the visitor abandons and for the beacon to not be blocked by tracker blockers.</p>
<p>Whilst there are strategies to improve our data collection for abandoners, we will always lose some data. The best method to reduce abandonment and increase captured traffic is... improving performance! There are two reasons for this:</p>
<ul>
<li>faster page loads mean that analytics events fire earlier and are more likely to capture the visitor</li>
<li>faster page loads are a better experience, so visitors are more likely to stay on the page</li>
</ul>
<p>Both of these are positive outcomes!</p>
<p>Whilst the focus in web performance has recently been on the <em>Core</em> Web Vitals, I encourage you to analyse your First Contentful Paint with as much scrutiny as Largest Contentful Paint, Cumulative Layout Shift or First Input Delay. FCP was used by Nicolás in the abandonment study because it is such an important indicator of user experience.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fcp_simon.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/LzIDaLh_8n-600.avif 600w, https://simonhearne.com/img/LzIDaLh_8n-900.avif 900w, https://simonhearne.com/img/LzIDaLh_8n-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/LzIDaLh_8n-600.webp 600w, https://simonhearne.com/img/LzIDaLh_8n-900.webp 900w, https://simonhearne.com/img/LzIDaLh_8n-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/LzIDaLh_8n-600.jpeg 600w, https://simonhearne.com/img/LzIDaLh_8n-900.jpeg 900w, https://simonhearne.com/img/LzIDaLh_8n-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Web Page Test filmstrip showing a gap of blank screen before first contentful paint" loading="lazy" decoding="async" src="https://simonhearne.com/img/LzIDaLh_8n-600.jpeg" width="1200" height="340" /></picture></a><figcaption>FCP marks when your visitor can see something useful</figcaption></figure>
<p>FCP marks the context switch from the previous page or a blank screen to showing something that the visitor actually expects to see, so in my analysis it is often the primary metric I focus on improving. If FCP is high, it means that visitors are more likely to think that the page is not working, they have time to reconsider what action they were taking and can easily hit the back button.</p>
<p>Note that FCP <a href="https://blog.webpagetest.org/posts/why-first-contentful-paint-doesnt-work-as-a-cross-browser-metric/">isn't a great cross-browser metric</a>, so your mileage may vary when tracking it over time and across browsers and operating systems. That does not necessarily mean that it is a bad metric, it's just complex to implement consistently across the different browser engines.</p>
<p>The key points for optimising FCP are universal, though:</p>
<ul>
<li>reduce time to first byte (TTFB): use a CDN, cache HTML pages</li>
<li>reduce blocking JS and CSS</li>
<li>defer non-critical JS</li>
<li>remove render blocking third-party tags</li>
</ul>
<p>Expect to see the unexpected in your real user analytics when shipping performance improvements! Technically reducing FCP may in fact increase your reported FCP as more visitors on low-end devices and connections successfully load your pages. Expect to see traffic numbers change for different device types, and combine your performance metrics with business metrics to get a more holistic view of the success of each technical performance improvement.</p>
<script>
let initialised = false;
let data = {'stats':{},'sampledStats':{},'sampleSize':10000,'sampledSize':0};
let checker = setInterval(()=>{
if (typeof(window.charts) == 'object') {
if (window.charts.filter(c => c.id == "vis2").length > 0) {
initilised = true;
initChartListeners();
clearInterval(checker);
}
}
},1000);
function initChartListeners() {
let view = window.charts.filter(c => c.id == "vis2")[0].view;
// set up view listeners for stats and sampledStats
view.addSignalListener('stats', chartListener);
view.addSignalListener('sampledStats', chartListener);
view.addSignalListener('sampledSize', chartListener);
data.stats = view.signal('stats');
data.sampledStats = view.signal('sampledStats');
data.sampledSize = view.signal('sampledSize');
data.sampleSize = view.signal('sampleSize');
renderStats();
}
function chartListener(name,value) {
data[name] = value;
renderStats();
}
function renderStats() {
let el = document.getElementById("chartStats");
el.innerHTML = `
<tr><th>Metric</th><th>Actual Visits</th><th>Excluding Abandons</th><th>Difference</th><th>Relative Difference</th></tr>
<tr><td>Visits</td> <td>${(data.sampleSize).toLocaleString()}</td> <td>${data.sampledSize.toLocaleString()}</td> <td>-${(data.sampleSize - data.sampledSize).toLocaleString()}</td><td>-${Math.round(((data.sampleSize - data.sampledSize)/data.sampleSize)*1000)/10}%</td></tr>
<tr><td>Median FCP</td> <td>${(Math.round(data.stats[0].value*100)/100).toLocaleString()}s</td><td>${(Math.round(data.sampledStats[0].value*100)/100).toLocaleString()}s</td><td>${(Math.round((data.sampledStats[0].value - data.stats[0].value)*1000)).toLocaleString()}ms</td><td>${Math.round(((data.sampledStats[0].value - data.stats[0].value)/data.sampledStats[0].value)*1000)/10}%</td></tr>
<tr><td>75% FCP</td> <td>${(Math.round(data.stats[1].value*100)/100).toLocaleString()}s</td><td>${(Math.round(data.sampledStats[1].value*100)/100).toLocaleString()}s</td><td>${(Math.round((data.sampledStats[1].value - data.stats[1].value)*1000)).toLocaleString()}ms</td><td>${Math.round(((data.sampledStats[1].value - data.stats[1].value)/data.sampledStats[1].value)*1000)/10}%</td></tr>
<tr><td>90% FCP</td> <td>${(Math.round(data.stats[2].value*100)/100).toLocaleString()}s</td><td>${(Math.round(data.sampledStats[2].value*100)/100).toLocaleString()}s</td><td>${(Math.round((data.sampledStats[2].value - data.stats[2].value)*1000)).toLocaleString()}ms</td><td>${Math.round(((data.sampledStats[2].value - data.stats[2].value)/data.sampledStats[2].value)*1000)/10}%</td></tr>
<tr><td>95% FCP</td> <td>${(Math.round(data.stats[3].value*100)/100).toLocaleString()}s</td><td>${(Math.round(data.sampledStats[3].value*100)/100).toLocaleString()}s</td><td>${(Math.round((data.sampledStats[3].value - data.stats[3].value)*1000)).toLocaleString()}ms</td><td>${Math.round(((data.sampledStats[3].value - data.stats[3].value)/data.sampledStats[3].value)*1000)/10}%</td></tr>
`;
}
</script>
The value of an independent web performance consultant2022-02-04T10:00:00Zhttps://simonhearne.com/2022/value-of-a-consultant/<p>I have been employed in web performance roles for about a decade. From load testing and synthetic monitoring to CDN configuration and true consultancy. In 2022 I became an independent web performance consultant.</p>
<p>One thing has stood out from my experience working with hundreds of brands — from small national websites to massive multi-national ecommerce websites: folks generally understand web performance, why it matters and the core principles, but they struggle to keep on top of industry trends whilst maintaining a complex production codebase. Sometimes all it takes is a call that brings together marketing, product and development teams to talk it through; other situations call for a multi-year project to gradually introduce optimisations and measure change.</p>
<p>Coming in as a consultant allows me to be a catalyst for positive change, even if all of the performance issues had previously been documented! Dedicated, independent consultants (like me, or others in the industry) help to bring focus and passion to a topic that is often under-valued and under-invested. This quote from Rob Kerr on self-employment resonates with me:</p>
<blockquote>I believe that, given the right preparation, self-employment can be empowering for the individual and can solve meaningful problems in the world. Many of these problems would never justify the investment at a corporate level, and nor could a big business deliver the solution to the same standard as the person who is passionate about making that difference.
<p class="align-right small">— <a href="https://www.scribd.com/book/484449345/Project-Future-6-Steps-to-Success-as-Your-Own-Boss">Project Future</a> by Rob Kerr</p>
</blockquote>
<p>In most organisations it does not make sense to employ a full-time individual who is solely focussed on site speed. Some of the more successful organisations I've worked with have web performance working groups - a cross-functional team who have a few days per month to discuss and prioritise performance improvements. Even in these cases, having an external view on site speed adds incredible value: reviewing the KPIs that are being measured, prioritising backlogged tasks, improving tooling and even identifying new issues that have been undetected by the in-house teams.</p>
<p>Independent performance consultants add further value because of the work we do across multiple clients: working in different codebases on different platforms for different types of organisation. We absorb best practices from multiple teams and can help apply them to each of our clients. If nothing else, an experienced second set of eyes on a website will always find something to improve.</p>
<p>Speaking of improvements: measuring impact is one of the most difficult parts of web performance in general, and is a real challenge when it comes to consultancy. I would love to be able to attribute performance optimisations back to real business impact, but this is near impossible in a world of daily releases, multiple campaigns per week and the rollercoaster of SEO / SEM. We know from <a href="https://wpostats.com/">hundreds of case studies</a> that improving performance improves user experience and business results, so most times we have to rely on that implicit understanding.</p>
<p>In an ideal world performance changes are <a href="https://blog.analytics-toolkit.com/2014/aa-aab-aabb-tests-cro/">A/A/B</a> tested to determine the impact on business results, using a real-user monitoring (RUM) solution like <a href="https://www.akamai.com/products/mpulse-real-user-monitoring">Akamai mPulse</a> or <a href="https://www.speedcurve.com/features/performance-monitoring/">SpeedCurve</a> to track performance + business metrics together. Even then, some performance improvements will impact metrics that are near-impossible to track: e.g. user satisfaction, return rates and likelihood to recommend. Improving performance <em>will</em> improve business metrics, it's just nearly impossible to measure the direct impact.</p>
<p><a href="https://twitter.com/ksylor">Katie Sylor-Miller</a> recently made the same observation about the difficulties in attributing positive performance changes to business results:</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">It feels like it's easy to detect & see business impact of performance *regressions*, but much harder to correctly measure & know when perf improves - especially when it significantly improves user experience👇🏻</p>— Katie Sylor-Miller (@ksylor) <a href="https://twitter.com/ksylor/status/1488657225744896005?ref_src=twsrc%5Etfw">February 1, 2022</a></blockquote>
<p>Another difficult part of web performance is determining the impact of a technical change at a user experience level. Reducing time to first byte (TTFB) by 500ms will have a very clear benefit: every experience, every pageview, will become a half a second quicker. But most changes — like reducing CSS bundle size, optimising images or reducing JavaScript execution times — will not have such a linear relationship.</p>
<p>Improving page load time by 100ms under test conditions may not seem like a lot, but at the tail-end of your user distribution this could result in more than a second of improvement. Users in more remote geographies, on older phones, on poorer quality networks will always see a bigger improvement than users on a flagship phone connected to a 100+Mbps network. Implementing a RUM solution and providing deep analysis of the data can help to illuminate the users having the worst experiences and to identify the most important optimisations to focus on.</p>
<p><strong>In summary</strong>: performance matters to user experience, even if it is difficult to prove. An independent performance consultant can help to bring teams together, to identify and prioritise performance enhancements and to measure their impact. As a bonus: onboarding a consultant is almost certainly quicker (and cheaper) than developing and maintaining in-house expertise to the same level - all while sharing knowledge internally and helping to build a performance culture.</p>
Caching Header Best Practices2022-01-27T19:00:00Zhttps://simonhearne.com/2022/caching-header-best-practices/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#introduction" aria-hidden="true">#</a></h2>
<p>Caching headers are one of those deceptively complex web technologies which are so often overlooked or misconfigured. The fastest request is the one that is not made, and caching headers allow us to tell browsers when they can reuse an asset that they have already downloaded. The reason that these headers are often misconfigured, or at least configured suboptimally, is often through a descent to lowest risk. Allowing a browser to use a cached asset can be considered risky - JS which falls out of sync with HTML, CSS which persists an old campaign style, personalised assets accidentally being shared between visitors.</p>
<p>In this post we will review what caching headers are available and when they should be used. We'll also talk about invalidating caches and ensuring browsers use the correct assets at the correct time. Note that the focus of this post is on client-side (or downstream) caching - in the client device. Caching in proxies, load balancers and Content Delivery Networks (CDNs) adds some more complexity, and is not covered here.</p>
<h3 id="the-solution" tabindex="-1">The Solution <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#the-solution" aria-hidden="true">#</a></h3>
<p>Use versioned assets wherever possible (e.g. <code>main.v123.min.css</code> or <code>main.min.css?v=123</code>) and set a single caching header allowing the maximum cache duration of one year:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=31536000, immutable</span></span></code></pre>
<p>For non-versioned assets which may change, combine the Cache-Control header with an ETag for asynchronous revalidation in the client:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=604800, stale-while-revalidate=86400</span></span><br /><span class="token header"><span class="token header-name keyword">ETag</span><span class="token punctuation">:</span> <span class="token header-value">"<file-hash-generated-by-server>"</span></span></code></pre>
<p>For HTML files, set a low TTL and private cache flags:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age:300, private</span></span></code></pre>
<p>Do not emit unnecessary caching headers (including <code>ETag</code> and <code>Last-Modified</code>) to prevent unexpected client and server behaviours.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/caching-header-best-practices/cache-headers.svg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_dzUob7MLk-600.avif 600w, https://simonhearne.com/img/_dzUob7MLk-900.avif 900w, https://simonhearne.com/img/_dzUob7MLk-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_dzUob7MLk-600.webp 600w, https://simonhearne.com/img/_dzUob7MLk-900.webp 900w, https://simonhearne.com/img/_dzUob7MLk-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_dzUob7MLk-600.jpeg 600w, https://simonhearne.com/img/_dzUob7MLk-900.jpeg 900w, https://simonhearne.com/img/_dzUob7MLk-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Flowchart of decisions to determine which caching headers are best in different scenarios. The logic is described fully in the document" loading="lazy" decoding="async" src="https://simonhearne.com/img/_dzUob7MLk-600.jpeg" width="1200" height="581" /></picture></a><figcaption>A summary of the logic to determine which caching headers are best in different scenarios.</figcaption></figure>
<h3 id="caching-headers" tabindex="-1">Caching Headers <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#caching-headers" aria-hidden="true">#</a></h3>
<p>There are a number of response headers set by a web server or CDN which manage client-side caching. Some are more obvious than others!</p>
<p><code>Expires</code> - a date (in GMT) after which this asset may no longer be used from the browsers cache and must be re-fetched (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires">docs</a>)</p>
<p><code>Cache-Control</code> - a combination of features in one header, including how long the resource can be cached by the client (in seconds) as well as whether proxies can cache it, whether to force revalidation and more (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">docs</a>)</p>
<p><code>ETag</code> - a string that uniquely identifies an asset version, generally a server-generated hash of the file (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag">docs</a>)</p>
<p><code>Last-Modified</code> - a timestamp which allows browsers to validate the freshness of cached assets (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified">docs</a>)</p>
<p><code>Pragma</code> - a hangover from HTTP/1.0, this should generally not be used in preference for <code>Cache-Control</code> except where HTTP/1.0 clients <em>must</em> be supported (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma">docs</a>)</p>
<p>In general I recommend to not emit an <code>Expires</code> header and rely instead on the more comprehensive <code>Cache-Control</code> header. I also recommend to not emit a <code>Last-Modified</code> header and use <code>ETag</code> instead for asset revalidation, this avoids edge cases such as newer files with identical content or clock mismatches between web servers which would cause unnecessary bandwidth consumption.</p>
<p>For revalidation (aka conditional requests) to work, responses must be served with one or both of the <code>ETag</code> or <code>Last-Modified</code> headers. The server must also understand conditional get requests and respond with a <code>304 Not Modified</code> in the case that the cached asset matches that on origin. Note that validation of weak ETags (prefixed by <code>W/</code> in the header) are unsupported in some scenarios — including if using Akamai as a CDN — so you may want to use strong ETags where possible.</p>
<blockquote class="callout">
<p>ETags are simply strings which identify a specific version of an asset. Weak ETags (prefixed with <code>W/</code>) should match assets which are semantically the same (e.g. metadata has been updated but the content is the same), whereas strong ETags should change whenever the asset is changed in any way. (See <a href="https://www.rfc-editor.org/rfc/rfc7232#section-2.1">RFC 7232</a> for more info!)</p>
<p>By default, Apache 2.3.14 and earlier included <code>INode</code> in the ETag - meaning that the ETag for an identical asset would change between servers! This is no longer included by default, and you can <a href="https://httpd.apache.org/docs/2.4/mod/core.html#FileETag">configure what is used to produce the ETag</a> in Apache. The default is to use the last-modified time and the file size, but you can also choose to use a file digest, and manually include the INode back into the calculation.</p>
<p>If you have your own versioning implemented on the web server you could generate ETags yourself. E.g. <code>ETag: core-js-es6-v13.1234-gzip</code>. But then you might as well rename the file to break the cache on the front-end.</p>
</blockquote>
<h2 id="caching-behaviours" tabindex="-1">Caching Behaviours <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#caching-behaviours" aria-hidden="true">#</a></h2>
<p>There are generally four types of caching behaviour we may want a browser to use when it has downloaded a static asset:</p>
<h3 id="1-not-cacheable" tabindex="-1">1. Not Cacheable <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#1-not-cacheable" aria-hidden="true">#</a></h3>
<p>For assets that are dynamically generated, that are unique or that are only valid once.</p>
<p><code>Cache-Control: no-cache</code></p>
<p>This directive tells the client that it can cache the asset, but it cannot use the cached asset without revalidating with the server. If the asset cannot be revalidated (i.e. there were no <code>ETag</code> or <code>Last-Modified</code> response headers on the asset) then the cached asset will never be used.</p>
<p><code>Cache-Control: no-store</code></p>
<p>This directive tells the client that the asset may not be stored in cache at all. Any further requests for this asset will be full requests back to the server.</p>
<h3 id="2-immutable" tabindex="-1">2. Immutable <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#2-immutable" aria-hidden="true">#</a></h3>
<p>These assets that can be stored by the browser indefinitely because they never change. Use this for versioned assets (e.g. <code>main.v123.min.css</code> or <code>main.min.css?v=123</code>).</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=31536000, immutable</span></span></code></pre>
<p>The <code>immutable</code> directive explicitly tells the browser (<a href="https://caniuse.com/mdn-http_headers_cache-control_immutable">where supported</a>) that the asset can be stored for a year and never needs to be revalidated.</p>
<h3 id="3-time-restricted" tabindex="-1">3. Time-restricted <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#3-time-restricted" aria-hidden="true">#</a></h3>
<p>For assets which should be stored for the duration of a session (e.g. one day or one week) but should be refreshed or revalidated if the visitor returns later.</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=86400</span></span></code></pre>
<h3 id="4-revalidated" tabindex="-1">4. Revalidated <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#4-revalidated" aria-hidden="true">#</a></h3>
<p>When combined with #3, this allows browsers to use a cached asset for a period of time (from zero seconds up to a year) and then revalidate the object with origin once that period has expired. <code>stale-while-revalidate</code> causes the revalidation request to happen asynchronously, improving performance at the potential risk of using stale content with a second time to live (TTL) value for how long the stale asset may be used.</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=604800, stale-while-revalidate=86400</span></span></code></pre>
<h2 id="optimal-caching-strategy" tabindex="-1">Optimal Caching Strategy <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#optimal-caching-strategy" aria-hidden="true">#</a></h2>
<p>In general we want browsers to cache everything forever. This can be achieved quite simply by setting a <code>Cache-Control: max-age=31536000</code> response header - using the maximum TTL value of one year. The issue is that your web applications likely change more frequently than yearly. This is where the most important feature of your web build pipeline comes in - versioned assets!</p>
<p>With versioned assets, the browser will automatically ignore stale cached assets as the references will be updated. Instead of the HTML document requesting <code>main.v123.min.css</code>, the reference will be to <code>main.v124.min.css</code> (note the incremented version number, this will be automatically generated at build time and may be an asset hash).</p>
<p>If assets will always be versioned, we can consider them <code>immutable</code>. That means that if the content of the file changes we guarantee that the filename is changed. Some browsers support this concept natively in the <code>cache-control</code> header, preventing revalidation requests ever being made for these assets:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=31536000, immutable</span></span></code></pre>
<h3 id="query-strings-and-caching" tabindex="-1">Query Strings and Caching <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#query-strings-and-caching" aria-hidden="true">#</a></h3>
<p>If an asset filename cannot be updated automatically, a query string parameter can be added to the URL to break the cache. For example <code>main.min.css?v=124</code>. This method should be robust in most cases, ensuring that your CDN configuration treats query strings as part of the asset cache key for caching at the CDN level:</p>
<ul>
<li>Cloudflare <a href="https://developers.cloudflare.com/cache/how-to/set-caching-levels">includes query strings by default</a></li>
<li>Akamai <a href="https://developer.akamai.com/blog/2017/04/14/what-you-need-know-about-caching-part-3#how-query-parameters-affect-cache-keys">excludes query strings by default</a></li>
<li>Fastly <a href="https://docs.fastly.com/en/guides/manipulating-the-cache-key">includes query strings by default</a></li>
</ul>
<h3 id="irregular-updates-to-unversioned-assets" tabindex="-1">Irregular Updates to Unversioned Assets <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#irregular-updates-to-unversioned-assets" aria-hidden="true">#</a></h3>
<p>One of the trickiest caching scenarios to manage is where an asset is unversioned (no unique identifier in the URL) and it is updated on an irregular schedule. This could be, for example, a document containing stock information, transport schedules or feature flags.</p>
<p>In this case, we still want the browser to be able to use the cached asset but also ensure that it remains relatively fresh. This is (in my opinion) the only scenario where entity tags (ETags) make sense. ETags should be used in combination with a valid <code>Cache-Control</code> header to ensure you have control over how the browser manages its cache state:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=86400, must-revalidate</span></span><br /><span class="token header"><span class="token header-name keyword">ETag</span><span class="token punctuation">:</span> <span class="token header-value">"a-unique-hash-generated-by-the-server"</span></span></code></pre>
<p>This example will allow the browser to use the cached asset for up to 24 hours (86,400 seconds), after which it must revalidate with the server. A revalidation request (also known as a conditional GET request) acts just like a normal request for the asset, with the addition of one or two request headers: <code>If-None-Match</code> where an ETag was present on the original response, <code>If-Modified-Since</code> in the case that a Last-Modified header was present:</p>
<pre class="language-http"><code class="language-http">GET /main.min.css<br /><br /><span class="token header"><span class="token header-name keyword">Accept</span><span class="token punctuation">:</span> <span class="token header-value">*/*</span></span><br /><span class="token header"><span class="token header-name keyword">Accept-Encoding</span><span class="token punctuation">:</span> <span class="token header-value">gzip,br</span></span><br /><span class="token header"><span class="token header-name keyword">If-None-Match</span><span class="token punctuation">:</span> <span class="token header-value">"a-unique-hash-generated-by-the-server"</span></span></code></pre>
<p>This <code>If-None-Match</code> header is a message to the server that the client has a version of the asset in cache. The server can then check to see whether this is still a valid version of the asset - if so, we will receive an empty <code>304</code> response with another ETag which will match the original:</p>
<pre class="language-http"><code class="language-http">304 Not Modified<br /><br /><span class="token header"><span class="token header-name keyword">ETag</span><span class="token punctuation">:</span> <span class="token header-value">"a-unique-hash-generated-by-the-server"</span></span></code></pre>
<p>If the asset has changed since the client cached it, we will get a full response with the new ETag:</p>
<pre class="language-http"><code class="language-http">200 Found<br /><br /><span class="token header"><span class="token header-name keyword">Content-Length</span><span class="token punctuation">:</span> <span class="token header-value">100000</span></span><br /><span class="token header"><span class="token header-name keyword">ETag</span><span class="token punctuation">:</span> <span class="token header-value">"a-NEW-unique-hash-generated-by-the-server"</span></span></code></pre>
<p>This approach is valuable, but does have drawbacks:</p>
<ul>
<li>Generating & maintaining asset hashes has a small compute cost on the server</li>
<li>Some web servers have bugs with ETag generation and validation, especially with multiple servers behind a load balancer</li>
<li>A <code>304</code> response will add a small front-end delay for each request and incurs a small amount of compute on the web server - the client will not use the (valid) cached asset until the <code>304</code> response is received</li>
</ul>
<p>A similar process occurs when the <code>Last-Modified</code> header is present on responses. You may see conditional requests sent with the <code>If-Modified-Since</code> header:</p>
<pre class="language-http"><code class="language-http">GET /main.min.css<br /><br /><span class="token header"><span class="token header-name keyword">If-Modified-Since</span><span class="token punctuation">:</span> <span class="token header-value">Wed, 21 Oct 2015 07:28:00 GMT</span></span></code></pre>
<p>If both <code>If-Modified-Since</code> and <code>If-None-Match</code> request headers are present then the server must only return a <code>304</code> if <strong>both</strong> conditional fields match the origin content. The <code>If-Modified-Since</code> header allows the server to check the last modified time of the requested asset and again return a <code>304 Not Modified</code> if it is the equal to or earlier than the timestamp in the request.</p>
<p>Using <code>Last-Modified</code> may cause more cache-misses than <code>ETag</code> if releases touch all files on the web server, even if they have no updates (updating the last modified time on all files). There is a benefit to using <code>Last-Modified</code> over <code>ETag</code> in the edge case where servers may hold older versions of an asset - the <code>ETag</code> will not match and result in a full <code>200</code> response, whereas the <code>Last-Modified</code> date will be later than the origin asset, resulting in an empty <code>304</code> response.</p>
<h2 id="what-about-html" tabindex="-1">What about HTML? <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#what-about-html" aria-hidden="true">#</a></h2>
<p>HTML assets are critical to performance in traditional (non-SPA) web applications. A 100ms delay downloading the HTML for a page will make everything else 100ms slower.</p>
<p>Folks are normally very nervous about allowing browsers to cache HTML though, for a number of good reasons:</p>
<ul>
<li>Personalisation in the page (e.g. user name, basket contents, geolocation logic)</li>
<li>Asset versions (e.g. <code>main.v123.min.css</code>) are updated in the HTML to purge the client cache on release</li>
<li>Rapid updates (e.g. a news publication with breaking stories)</li>
</ul>
<p>Even with all of these considerations, caching HTML can still be achieved and performance can be significantly improved. We just need to be careful!</p>
<h3 id="short-ttls-and-private-caches" tabindex="-1">Short TTLs & private caches <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#short-ttls-and-private-caches" aria-hidden="true">#</a></h3>
<p>Allowing an HTML asset to be cached for a short period can improve user experience with minimal risk. Setting a TTL of five minutes (300s) for example should be fine, you can also add <code>must-revalidate</code> to ensure that browsers do not use a stale version of the asset after the TTL has expired. This will benefit visitors who click links returning them to previously visited pages, this does not affect the <a href="https://web.dev/bfcache/">back/forward cache</a>.</p>
<p>The <code>Cache-Control</code> header offers the <code>private</code> attribute, indicating that assets should not be stored by any proxies or CDNs, but may be cached by the client. This attribute enables you to allow browsers to cache personalised content - as the cached asset will not be shared across multiple visitors.</p>
<h3 id="remove-dynamic-elements-from-the-html-document" tabindex="-1">Remove dynamic elements from the HTML document <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#remove-dynamic-elements-from-the-html-document" aria-hidden="true">#</a></h3>
<p>Dynamic elements such as a basket count, user name, logged in / out status indicator all make caching HTML more tricky. Externalising these out to API calls, JavaScript and localStorage will mean that the base HTML template can be cached in the browser, with dynamic elements updated as the page is building. This will also allow pages to be cached by CDNs - greatly reducing Time to First Byte (TTFB) and thus improving experience.</p>
<p>This concept can be taken further in some scenarios, such as when anonymous visitors are presented generic / un-personalised pages. In this case, you can detect the anonymous visitor (e.g. by presence of a user cookie) and serve them a cached page from the CDN, whereas logged-in / recognised visitors will be served by origin to allow personalisation code to run. The possibilities here are quite exciting!</p>
<h2 id="general-recommendations" tabindex="-1">General Recommendations <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#general-recommendations" aria-hidden="true">#</a></h2>
<p>Explicitly set the <code>Cache-Control</code> header on all responses. Do not set the following response headers in most scenarios:</p>
<ul>
<li><code>Last-Modified</code></li>
<li><code>Expires</code></li>
<li><code>ETag</code></li>
<li><code>Pragma</code></li>
</ul>
<p>Always set the <code>Cache-Control</code> header, preferably with the value <code>max-age=31536000,immutable</code> alongside unique asset filenames (or <code>no-cache</code> for non-cacheable assets).</p>
<p>In some scenarios, use ETags to allow browsers to revalidated cached content using the following headers (e.g. cache for one week, allow stale assets with async revalidation for up to one day after cache expiry):</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">max-age=604800,stale-while-revalidate=86400</span></span><br /><span class="token header"><span class="token header-name keyword">ETag</span><span class="token punctuation">:</span> <span class="token header-value">"<generated-by-server>"</span></span></code></pre>
<p>The lack of <code>Last-Modified</code> and <code>ETag</code> headers on a response prevents the browser from making conditional GET requests, potentially reducing back-end load.</p>
<p>Ensure that your web application server / load-balancer / CDN handles conditional GET requests correctly and returns a <code>304 Not Modified</code> response when ETags match. Use <code>cURL</code> or <a href="http://redbot.org/">redbot.org</a> to test this.</p>
<h2 id="how-to-check-your-headers" tabindex="-1">How to check your headers <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#how-to-check-your-headers" aria-hidden="true">#</a></h2>
<p>Throughout this post I have detailed headers that you might see when requesting assets - of course these aren't visible on the web application directly! There are two key methods to check the response headers of your assets: using the browser and using command-line tools.</p>
<h3 id="browser-developer-tools" tabindex="-1">Browser developer tools <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#browser-developer-tools" aria-hidden="true">#</a></h3>
<p>All browsers have developer tools to help analyse network traffic, in Google Chrome you can press <span class="key">⌘</span> + <span class="key">⌥</span> + <span class="key">i</span> ( <span class="key">ctrl</span> + <span class="key">shift</span> + <span class="key">i</span> on Windows) to bring up Chrome Developer Tools, it will default to the last open tab so you may need to select the network tab. The network tab only records traffic whilst open, so reload the page to view the requests if none are present.</p>
<p>Once you have some traffic, click on a request to see more details:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/caching-header-best-practices/header-chrome-devtools.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/FX6IYiRr6V-600.avif 600w, https://simonhearne.com/img/FX6IYiRr6V-900.avif 900w, https://simonhearne.com/img/FX6IYiRr6V-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/FX6IYiRr6V-600.webp 600w, https://simonhearne.com/img/FX6IYiRr6V-900.webp 900w, https://simonhearne.com/img/FX6IYiRr6V-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/FX6IYiRr6V-600.jpeg 600w, https://simonhearne.com/img/FX6IYiRr6V-900.jpeg 900w, https://simonhearne.com/img/FX6IYiRr6V-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome developer tools showing response headers" loading="lazy" decoding="async" src="https://simonhearne.com/img/FX6IYiRr6V-600.jpeg" width="1200" height="668" /></picture></a><figcaption>Chrome Developer Tools showing response headers</figcaption></figure>
<p>If this is a task that you will repeat often, I recommend adding the relevant response headers to the network table. Right-click on the column headers and select <code>Response Headers</code>, <code>Cache-Control</code> should be there by default and you can also add custom headers here.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/caching-header-best-practices/header-chrome-column.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/DqSW6aYPi9-600.avif 600w, https://simonhearne.com/img/DqSW6aYPi9-900.avif 900w, https://simonhearne.com/img/DqSW6aYPi9-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/DqSW6aYPi9-600.webp 600w, https://simonhearne.com/img/DqSW6aYPi9-900.webp 900w, https://simonhearne.com/img/DqSW6aYPi9-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/DqSW6aYPi9-600.jpeg 600w, https://simonhearne.com/img/DqSW6aYPi9-900.jpeg 900w, https://simonhearne.com/img/DqSW6aYPi9-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome developer tools showing response headers in a column" loading="lazy" decoding="async" src="https://simonhearne.com/img/DqSW6aYPi9-600.jpeg" width="1200" height="315" /></picture></a><figcaption>Chrome Developer Tools showing a column for Cache-Control headers</figcaption></figure>
<h3 id="command-line-tools" tabindex="-1">Command-line tools <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#command-line-tools" aria-hidden="true">#</a></h3>
<p><a href="https://curl.se/">cURL</a> is one of the most widely deployed command line tools and is ideal for checking response headers. A simple command such as <code>curl 'https://simonhearne.com/css/main.css?v=4.1' -Is</code> will show you all response headers for the object, although we can build on this to focus just on the headers that we care about for caching (note that I have added some required request headers <code>user-agent</code> and <code>accept-language</code> so that a valid response is provided):</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token string">'https://phoenixnap.com/kb/wp-includes/css/dist/block-library/style.min.css?ver=5.9'</span> <span class="token punctuation">\</span><br /> <span class="token parameter variable">-H</span> <span class="token string">'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36'</span> <span class="token punctuation">\</span><br /> <span class="token parameter variable">-H</span> <span class="token string">'accept-language: en-GB,en-US;q=0.9,en;q=0.8'</span> <span class="token punctuation">\</span><br /> <span class="token parameter variable">--compressed</span> <span class="token parameter variable">-Is</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-E</span> <span class="token string">'cache-control|etag|last-modified|expires|pragma'</span><br /><br />last-modified: Wed, <span class="token number">26</span> Jan <span class="token number">2022</span> 05:24:24 GMT<br />etag: W/<span class="token string">"61f0db08-1357b"</span><br />expires: Thu, <span class="token number">26</span> Jan <span class="token number">2023</span> <span class="token number">11</span>:29:02 GMT<br />cache-control: max-age<span class="token operator">=</span><span class="token number">31536000</span><br />pragma: public<br />cache-control: max-age<span class="token operator">=</span><span class="token number">31536000</span>, public</code></pre>
<p>For this example, I would recommend that the website owner investigates the various response headers here which may conflict with each other. This set of six response headers could be replaced with a single header: <code>Cache-Control: max-age=31536000</code>! Redbot also shows a number of warnings for this implementation:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/caching-header-best-practices/redbot.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/kLf-H936jw-600.avif 600w, https://simonhearne.com/img/kLf-H936jw-900.avif 900w, https://simonhearne.com/img/kLf-H936jw-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/kLf-H936jw-600.webp 600w, https://simonhearne.com/img/kLf-H936jw-900.webp 900w, https://simonhearne.com/img/kLf-H936jw-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/kLf-H936jw-600.jpeg 600w, https://simonhearne.com/img/kLf-H936jw-900.jpeg 900w, https://simonhearne.com/img/kLf-H936jw-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of redbot showing warnings about response headers" loading="lazy" decoding="async" src="https://simonhearne.com/img/kLf-H936jw-600.jpeg" width="1200" height="652" /></picture></a><figcaption>Redbot showing potential issues with response headers (<a href="https://redbot.org/?uri=https%3A%2F%2Fphoenixnap.com%2Fkb%2Fwp-includes%2Fcss%2Fdist%2Fblock-library%2Fstyle.min.css%3Fver%3D5.9">view on redbot.org</a>)</figcaption></figure>
<h2 id="how-to-change-response-headers" tabindex="-1">How to change response headers <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#how-to-change-response-headers" aria-hidden="true">#</a></h2>
<p>Once you have determined your caching header strategy and reviewed the headers that are currently being emitted - it's time to make some changes!</p>
<p>Response headers can be set at multiple stages depending on your application architecture - for example the web server, load balancer and CDN can all set and modify response headers. Generally we should expect response headers to be forwarded transparently through proxies such as load balancers and CDNs unless explicit changes have been made, so the web server is the best place to start. You should be able to find what you need with a search for <code><web server name> set response headers</code> or <code><web server name> set caching headers</code> on your search engine of choice. For convenience, here are the relevant docs pages for some major web servers, PaaS / SaaS and CDN solutions:</p>
<h3 id="web-servers" tabindex="-1">Web Servers <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#web-servers" aria-hidden="true">#</a></h3>
<ul>
<li>Apache
<ul>
<li><a href="https://httpd.apache.org/docs/2.4/mod/core.html#FileETag">ETags in Core</a></li>
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_headers.html">mod_headers</a></li>
<li><a href="https://httpd.apache.org/docs/2.4/mod/mod_cache.html">mod_cache</a></li>
</ul>
</li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-implement-browser-caching-with-nginx-s-header-module-on-ubuntu-16-04">nginx</a></li>
<li><a href="https://h2o.examp1e.net/configure/headers_directives.html">h2o</a></li>
</ul>
<h3 id="paas-saas" tabindex="-1">PaaS / SaaS <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#paas-saas" aria-hidden="true">#</a></h3>
<ul>
<li><a href="https://docs.netlify.com/routing/headers/">Netlify</a> - note that there is an <a href="https://answers.netlify.com/t/setting-response-headers-only-on-documents/6144/40">open issue</a> regarding setting headers by content-type.</li>
<li><a href="https://support.wix.com/en/article/site-performance-caching-pages-to-optimize-loading-speed">Wix</a> - note that Wix does not provide fine-grained control over caching headers</li>
<li><a href="https://wpengine.com/support/cache-control-headers-wp-engine/">WPengine</a></li>
<li><a href="https://discourse.webflow.com/t/setting-http-header-fields/180953/4">Webflow</a> - it doesn't look like you can control headers and the recommendation is to use a CDN in front of Webflow</li>
</ul>
<h3 id="cdns" tabindex="-1">CDNs <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#cdns" aria-hidden="true">#</a></h3>
<ul>
<li><a href="https://techdocs.akamai.com/api-gateway/docs/caching#downstream-caching">Akamai</a></li>
<li><a href="https://docs.fastly.com/en/guides/controlling-caching">Fastly</a></li>
<li><a href="https://developers.cloudflare.com/cache/how-to/set-browser-ttl">Cloudflare</a> - lets you set a TTL but no fine-grained control over response headers</li>
<li><a href="https://developers.cloudflare.com/pages/platform/headers">Cloudflare Pages</a></li>
<li><a href="https://developers.cloudflare.com/workers/examples/alter-headers">Cloudflare Workers</a> - I use a worker in front of Netlify to <a href="https://answers.netlify.com/t/setting-response-headers-only-on-documents/6144/40">overcome limitations</a> in netlify.toml. Provides absolute control over response headers.</li>
</ul>
<h2 id="a-note-on-cache-control-public" tabindex="-1">A note on Cache-Control: public <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#a-note-on-cache-control-public" aria-hidden="true">#</a></h2>
<p>A lot of resources (e.g. on <a href="https://web.dev/http-cache/">web.dev</a>) recommend setting the <code>public</code> attribute on <code>Cache-Control</code> headers if resources can be shared between visitors. This is potentially misleading as <code>public</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#public">is the default behaviour</a> where <code>private</code> is not set.</p>
<p>The <code>public</code> attribute should <strong>not</strong> be set in almost all cases, as it can lead to an issue with authorized requests. Setting the <code>public</code> attribute in a <code>Cache-Control</code> header for an authorized request (i.e. one which is made with an <code>Authorization</code> header) will explicitly allow intermediary servers to store and serve the response to other visitors. In most cases this is not the expected behaviour and could lead to data leakage.</p>
<h2 id="in-closing" tabindex="-1">In closing… <a class="direct-link" href="https://simonhearne.com/2022/caching-header-best-practices/#in-closing" aria-hidden="true">#</a></h2>
<p>Client-side caching is a key technique to improving front-end speed and user experience. Whilst it may appear complex and risky, investing the time to review your content and setting the correct response headers will reduce bandwidth utilisation and improve speed for return visitors as well as mid-session.</p>
<p>There are multiple methods to manage caching, in this article I have presented a preference for <code>Cache-Control</code> (and <code>ETag</code> where appropriate). Exact implementation is not as important as correctness and consistency, though. Use what works for your application architecture, environment and processes.</p>
Fast and Responsive Hero Videos for Great UX2021-11-02T10:00:00Zhttps://simonhearne.com/2021/fast-responsive-videos/<p>Landing page hero videos are increasingly popular, especially in high fashion and luxury goods. Hero videos can have a detrimental effect on user experience though, delaying page load times and causing the page to jump around. In this post we will take an example homepage and see how we can optimise it to deliver fast, responsive videos for great UX.</p>
<p>Jump straight to the <a href="https://simonhearne.com/2021/fast-responsive-videos/#conclusion">conclusion</a> to see the results for each optimisation (in total we halve the video load time) and see the final working demo with code!</p>
<figure class="no-shadow">
<div class="cols-2">
<img src="https://simonhearne.com/images/fast-responsive-videos/phone.jpg" alt="" loading="lazy" clickable="false" maxwidth="300" />
<img src="https://simonhearne.com/images/fast-responsive-videos/ipad.jpg" alt="" loading="lazy" clickable="false" maxwidth="300" />
</div>
<figcaption>The example website uses different aspect ratio videos for narrow and wide viewports</figcaption>
</figure>
<p>Consider this simple homepage with a hero video and content below the video. The video has been created in two aspect ratios: one for mobile / portrait viewports and another for desktop / landscape viewports. It is common to see this developed along the following lines:</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- ==== homepage.html ==== --></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>video</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>full-width-video<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hero-video<span class="token punctuation">"</span></span><br /> <span class="token attr-name">playsinline</span> <span class="token attr-name">muted</span> <span class="token attr-name">loop</span> <span class="token attr-name">autoplay</span><br /> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.mp4<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-src-mobile</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile.mp4<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-src-desktop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.mp4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video-error<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Uhoh, no video for you.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>video</span><span class="token punctuation">></span></span></code></pre>
<pre class="language-css"><code class="language-css"><span class="token comment">/* ==== style.css ==== */</span><br /><br /><span class="token selector">.full-width-video</span> <span class="token punctuation">{</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br /> <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<pre class="language-js"><code class="language-js"><span class="token comment">/* ==== app.js ==== */</span><br /><br /><span class="token keyword">const</span> <span class="token constant">DESKTOP_BREAKPOINT</span> <span class="token operator">=</span> <span class="token number">480</span><span class="token punctuation">;</span><br />window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"load"</span><span class="token punctuation">,</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> heroVideoEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementByID</span><span class="token punctuation">(</span><span class="token string">"hero-video"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">let</span> heroVideoSrc <span class="token operator">=</span> heroVideoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>srcMobile<br /> <span class="token keyword">let</span> viewportWidth <span class="token operator">=</span> window<span class="token punctuation">.</span>innerWidth<span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>viewportWidth <span class="token operator">>=</span> <span class="token constant">DESKTOP_BREAKPOINT</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> heroVideoSrc <span class="token operator">=</span> heroVideoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>srcDesktop<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> heroVideoEl<span class="token punctuation">.</span>src <span class="token operator">=</span> heroVideoSrc<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>Note that the <code><video></code> element no size attributes and has a <code>src</code> which will be replaced for mobile visitors. This causes two types of issue:</p>
<p><strong>Performance</strong></p>
<ol>
<li>The desktop video will likely always be requested, even for mobile visitors who will never see it. The browser should cancel the request when the <code>src</code> is replaced, at least.</li>
<li>The mobile video doesn't start downloading until after app.js has downloaded, parsed and executed - delaying the mobile experience. On a 3G connection we must wait six seconds to see the first frame of the video!</li>
</ol>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/original.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/mrWsZCyuBK-600.avif 600w, https://simonhearne.com/img/mrWsZCyuBK-900.avif 900w, https://simonhearne.com/img/mrWsZCyuBK-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/mrWsZCyuBK-600.webp 600w, https://simonhearne.com/img/mrWsZCyuBK-900.webp 900w, https://simonhearne.com/img/mrWsZCyuBK-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/mrWsZCyuBK-600.jpeg 600w, https://simonhearne.com/img/mrWsZCyuBK-900.jpeg 900w, https://simonhearne.com/img/mrWsZCyuBK-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/mrWsZCyuBK-600.jpeg" width="1200" height="361" /></picture></a><figcaption>Slow hero videos can detract from landing page experience.</figcaption></figure>
<p><strong>Layout</strong></p>
<p>The browser cannot allocate the correct vertical space for the video until the first chunk is downloaded, causing an unpleasant layout shift at 5.5s in the filmstrip above.</p>
<p>The following four sections will take us from this six second experience to under three seconds, and with no layout shifts!</p>
<h2 id="optimisation-1-preventing-the-layout-shift" tabindex="-1">Optimisation 1: Preventing the Layout Shift <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#optimisation-1-preventing-the-layout-shift" aria-hidden="true">#</a></h2>
<p>The layout shift above is caused by the lack of size information on the video element. In this case there are two different video aspect ratios - wide for desktop and narrow for mobile - so we cannot simply add width and height attributes to the <code><video></code> element.</p>
<blockquote class="success"><p><em>aspect-ratio</em> to the rescue!</p></blockquote>
<p><code>aspect-ratio</code> in CSS has only recently gained <a href="https://caniuse.com/mdn-css_properties_aspect-ratio">widespread browser support</a>, but it is now safe to use across all evergreen browsers and Safari! This makes it simple to resolve the layout shift with just a few lines of CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token comment">/* ==== style.css ==== */</span><br /><br /><span class="token comment">/* default to mobile */</span><br /><span class="token selector">.full-width-video</span> <span class="token punctuation">{</span><br /> <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br /> <span class="token property">background-color</span><span class="token punctuation">:</span> black<span class="token punctuation">;</span><br /> <span class="token property">aspect-ratio</span><span class="token punctuation">:</span> 1280 / 960<span class="token punctuation">;</span> <span class="token comment">/* width and height of the mobile video */</span><br /><span class="token punctuation">}</span><br /><span class="token comment">/* override for desktop */</span><br /><span class="token atrule"><span class="token rule">@media</span> <span class="token keyword">only</span> screen <span class="token keyword">and</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 480px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br /> <span class="token selector">.full-width-video</span> <span class="token punctuation">{</span><br /> <span class="token property">aspect-ratio</span><span class="token punctuation">:</span> 1280 / 720<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>The result is that the browser knows how much space to allocate for the video, for both aspect-ratios, as soon as it is able to render the first frame. No more layout shift!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/aspect-ratio.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/bF54w3YyFR-600.avif 600w, https://simonhearne.com/img/bF54w3YyFR-900.avif 900w, https://simonhearne.com/img/bF54w3YyFR-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/bF54w3YyFR-600.webp 600w, https://simonhearne.com/img/bF54w3YyFR-900.webp 900w, https://simonhearne.com/img/bF54w3YyFR-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/bF54w3YyFR-600.jpeg 600w, https://simonhearne.com/img/bF54w3YyFR-900.jpeg 900w, https://simonhearne.com/img/bF54w3YyFR-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/bF54w3YyFR-600.jpeg" width="1200" height="675" /></picture></a><figcaption>A few lines of CSS removes layout shift entirely (<a href="https://www.webpagetest.org/video/compare.php?tests=211029_AiDc47_80cb876c7d79b44040a1c327d8e366b2%2C211029_AiDcR8_e2e6aa2957e48f3c24d48af9f0085134&thumbSize=150&ival=500&end=full">view filmstrip</a>)</figcaption></figure>
<p>We haven't yet made the video load faster, though...</p>
<h2 id="optimisation-2-better-source-selection" tabindex="-1">Optimisation 2: Better source selection <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#optimisation-2-better-source-selection" aria-hidden="true">#</a></h2>
<p>We're currently over-downloading on mobile by requesting the desktop video, then switching it out when our application JavaScript runs. Note that the desktop video continues to download until after <code>app1.js</code> has completely loaded at 3.6s.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/over-downloading.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/Wv2Abf0ufT-600.avif 600w, https://simonhearne.com/img/Wv2Abf0ufT-900.avif 900w, https://simonhearne.com/img/Wv2Abf0ufT-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Wv2Abf0ufT-600.webp 600w, https://simonhearne.com/img/Wv2Abf0ufT-900.webp 900w, https://simonhearne.com/img/Wv2Abf0ufT-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/Wv2Abf0ufT-600.jpeg 600w, https://simonhearne.com/img/Wv2Abf0ufT-900.jpeg 900w, https://simonhearne.com/img/Wv2Abf0ufT-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Wv2Abf0ufT-600.jpeg" width="1200" height="474" /></picture></a><figcaption>Late-running JavaScript means that both Desktop and Mobile videos download</figcaption></figure>
<p>Ideally, we would use native browser support to select the desktop or mobile video to avoid using JavaScript and remove the redundant request on mobile. Unfortunately this is not as easy as it should be. Intuitively we would use the familiar media query to determine which video version to load:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>video</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>source</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.mp4<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/mp4<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all and (min-width:480px)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>source</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile.mp4<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video/mp4<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all and (max-width:479px)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>video</span><span class="token punctuation">></span></span></code></pre>
<p>Whilst this was once possible, it has now been <a href="http://blog.greggant.com/posts/2019/08/02/responsive-videos-javascript-solution-video-source-tag.html">removed from the spec</a> and no browsers support it (except WebKit). So for now, we must use JavaScript. But we <strong>can</strong> still improve on the initial implementation!</p>
<p>The initial implementation had application code to set the video source that ran late in the page, well after the first paint. Moving the logic to be adjacent to the video will ensure that the code runs immediately after the video element is parsed by the browser, bringing the video render earlier.</p>
<p>First, we should remove the potentially wasteful desktop <code>src</code> value, then move the source selection logic out of app.js and into the HTML. I'm not generally a fan of inline scripts, but this is exactly the type of scenario where it makes sense. We can wrap this logic in a function and call it on resize to ensure that the correct video is always rendered. You will also want to add a <code><noscript></code> container with a fallback video at this point.</p>
<blockquote class="info"><p>November 4, 2021: Updated based on <a href="https://twitter.com/anthony_ricaud/status/1456327040370716672">Anthony Ricaud's feedback</a>, thanks Anthony!</p></blockquote>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- ==== homepage.html ==== --></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>video</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>full-width-video<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hero-video<span class="token punctuation">"</span></span><br /> <span class="token attr-name">playsinline</span> <span class="token attr-name">muted</span> <span class="token attr-name">loop</span> <span class="token attr-name">autoplay</span><br /> <span class="token attr-name">data-src-mobile</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile.mp4<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-src-desktop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.mp4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video-error<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Uhoh, no video for you.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>video</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">const</span> <span class="token constant">WIDE_VIEWPORT</span> <span class="token operator">=</span> <span class="token number">480</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">function</span> <span class="token function">resizeVideo</span><span class="token punctuation">(</span><span class="token parameter">wideViewport</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> videoEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"hero-video"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">let</span> src <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>mobileSrc<span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>wideViewport<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> src <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>desktopSrc<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>videoEl<span class="token punctuation">.</span>src <span class="token operator">!==</span> src<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> videoEl<span class="token punctuation">.</span>src <span class="token operator">=</span> src<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> wideViewport <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">(min-width: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">WIDE_VIEWPORT</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">px)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /> wideViewport<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>resizeVideo<span class="token punctuation">)</span><br /> <span class="token function">resizeVideo</span><span class="token punctuation">(</span>wideViewport<span class="token punctuation">)</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>The result is a slightly faster render, both of the page and the first frame of the video.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/inline-js.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/ZWhYErONzJ-600.avif 600w, https://simonhearne.com/img/ZWhYErONzJ-900.avif 900w, https://simonhearne.com/img/ZWhYErONzJ-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ZWhYErONzJ-600.webp 600w, https://simonhearne.com/img/ZWhYErONzJ-900.webp 900w, https://simonhearne.com/img/ZWhYErONzJ-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ZWhYErONzJ-600.jpeg 600w, https://simonhearne.com/img/ZWhYErONzJ-900.jpeg 900w, https://simonhearne.com/img/ZWhYErONzJ-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/ZWhYErONzJ-600.jpeg" width="1200" height="682" /></picture></a><figcaption>A few lines of inline JS improves render time slightly (<a href="https://www.webpagetest.org/video/compare.php?tests=211029_AiDcR8_e2e6aa2957e48f3c24d48af9f0085134%2C211029_AiDc6T_94eca459d8cd75434e40dafea045fdb8&thumbSize=150&ival=500&end=full">view filmstrip</a>)</figcaption></figure>
<p>Removing the redundant download of the desktop video also saves some transmitted bytes, 350kB in this test.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/difference.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/-npjGGUimM-600.avif 600w, https://simonhearne.com/img/-npjGGUimM-900.avif 900w, https://simonhearne.com/img/-npjGGUimM-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/-npjGGUimM-600.webp 600w, https://simonhearne.com/img/-npjGGUimM-900.webp 900w, https://simonhearne.com/img/-npjGGUimM-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/-npjGGUimM-600.jpeg 600w, https://simonhearne.com/img/-npjGGUimM-900.jpeg 900w, https://simonhearne.com/img/-npjGGUimM-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/-npjGGUimM-600.jpeg" width="1200" height="825" /></picture></a><figcaption>350kB of unnecessary video bytes removed with the inline-JS change</figcaption></figure>
<p>We are seeing some improvement, but 5.5s is still slow!</p>
<h2 id="optimisation-3-poster-images" tabindex="-1">Optimisation 3: Poster Images <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#optimisation-3-poster-images" aria-hidden="true">#</a></h2>
<p>The HTML5 <code><video></code> element supports the <code>poster</code> attribute, this lets us define an image to use as a placeholder until the browser has downloaded enough of the video to start playing it. This is a more elegant solution than attempting to preload the entire video file, preserving early bandwidth for more important assets such as CSS and JavaScript.</p>
<p>Placeholder images can be heavily optimised as they are only shown for a short period of time. Use a gaussian blur and a high image compression level to get the image size to around 10kB for each video.</p>
<p>We'll add our optimised placeholder images using the same dataset logic that we have used for the video files:</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- ==== homepage.html ==== --></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>video</span><br /> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>full-width-video<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hero-video<span class="token punctuation">"</span></span><br /> <span class="token attr-name">playsinline</span> <span class="token attr-name">muted</span> <span class="token attr-name">loop</span> <span class="token attr-name">autoplay</span><br /> <span class="token attr-name">data-src-mobile</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile.mp4<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-src-desktop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.mp4<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-poster-mobile</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile.jpg<span class="token punctuation">"</span></span><br /> <span class="token attr-name">data-poster-desktop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop.jpg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>video-error<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Uhoh, no video for you.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>video</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /> <span class="token keyword">const</span> <span class="token constant">WIDE_VIEWPORT</span> <span class="token operator">=</span> <span class="token number">480</span><span class="token punctuation">;</span><br /><br /> <span class="token keyword">function</span> <span class="token function">resizeVideo</span><span class="token punctuation">(</span><span class="token parameter">wideViewport</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">let</span> videoEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"hero-video"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">let</span> src <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>mobileSrc<span class="token punctuation">;</span><br /> <span class="token keyword">let</span> poster <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>mobilePoster<span class="token punctuation">;</span><br /> <span class="token keyword">let</span> widthDisplay <span class="token operator">=</span> <span class="token string">'MOBILE'</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>wideViewport<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> src <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>desktopSrc<span class="token punctuation">;</span><br /> poster <span class="token operator">=</span> videoEl<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>desktopPoster<span class="token punctuation">;</span><br /> widthDisplay <span class="token operator">=</span> <span class="token string">'DESKTOP'</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>videoEl<span class="token punctuation">.</span>src <span class="token operator">!==</span> src<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> videoEl<span class="token punctuation">.</span>src <span class="token operator">=</span> src<span class="token punctuation">;</span><br /> videoEl<span class="token punctuation">.</span>poster <span class="token operator">=</span> poster<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> wideViewport <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">(min-width: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">WIDE_VIEWPORT</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">px)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> wideViewport<span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>resizeVideo<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token function">resizeVideo</span><span class="token punctuation">(</span>wideViewport<span class="token punctuation">)</span><span class="token punctuation">;</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>The result of this is the biggest improvement we've seen so far - almost two whole seconds faster! This is especially important for low bandwidth users who may not see the video play for another ten seconds after the poster image is shown.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/posters.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/58SQHZVvdm-600.avif 600w, https://simonhearne.com/img/58SQHZVvdm-900.avif 900w, https://simonhearne.com/img/58SQHZVvdm-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/58SQHZVvdm-600.webp 600w, https://simonhearne.com/img/58SQHZVvdm-900.webp 900w, https://simonhearne.com/img/58SQHZVvdm-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/58SQHZVvdm-600.jpeg 600w, https://simonhearne.com/img/58SQHZVvdm-900.jpeg 900w, https://simonhearne.com/img/58SQHZVvdm-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/58SQHZVvdm-600.jpeg" width="1200" height="745" /></picture></a><figcaption>Valid poster images dramatically improve perceived performance (<a href="https://www.webpagetest.org/video/compare.php?tests=211029_AiDc6T_94eca459d8cd75434e40dafea045fdb8%2C211102_AiDcW9_bdf669fc640676266aab337d1a04e41e&thumbSize=150&ival=500&end=full">view filmstrip</a>)</figcaption></figure>
<h2 id="optimisation-4-preload-the-poster-image" tabindex="-1">Optimisation 4: Preload the Poster Image <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#optimisation-4-preload-the-poster-image" aria-hidden="true">#</a></h2>
<p>If you look carefully at the waterfall chart of the last test, you'll notice that the poster image is requested quite late - after the blocking script has downloaded and executed.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/late-poster.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/tpH5X_amOe-600.avif 600w, https://simonhearne.com/img/tpH5X_amOe-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/tpH5X_amOe-600.webp 600w, https://simonhearne.com/img/tpH5X_amOe-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/tpH5X_amOe-600.jpeg 600w, https://simonhearne.com/img/tpH5X_amOe-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/tpH5X_amOe-600.jpeg" width="900" height="384" /></picture></a><figcaption>Poster images delayed</figcaption></figure>
<p>We can help the browser here by using preload hints, directives which instruct the browser to download files that it hasn't yet discovered:</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- ==== homepage.html ==== --></span><br /> <span class="token comment"><!-- preloads should be at the bottom of the head --></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preload<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>desktop_poster.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">as</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all and (min-width:480px)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preload<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mobile__poster.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">as</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all and (max-width:479px)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span></code></pre>
<p>These allow the browser to request the poster images early, and do support media queries so that we can fetch the correct image! The result is a slightly faster render of the poster image:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/preloads.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/QWILqrlZkF-600.avif 600w, https://simonhearne.com/img/QWILqrlZkF-900.avif 900w, https://simonhearne.com/img/QWILqrlZkF-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/QWILqrlZkF-600.webp 600w, https://simonhearne.com/img/QWILqrlZkF-900.webp 900w, https://simonhearne.com/img/QWILqrlZkF-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/QWILqrlZkF-600.jpeg 600w, https://simonhearne.com/img/QWILqrlZkF-900.jpeg 900w, https://simonhearne.com/img/QWILqrlZkF-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/QWILqrlZkF-600.jpeg" width="1200" height="524" /></picture></a><figcaption>Preload poster images for the final performance gains!</figcaption></figure>
<p>Another alternative that I have seen used is to Base64 encode the poster image and insert it directly into the attribute. I am not a fan of this method: it bloats the HTML document and causes the browser to download unnecessary content (at least one of the poster images is not required). I would not recommend this unless the encoded images are under 1kB, for example a simple play icon on a plain background.</p>
<p>It may be possible to further improve performance, specifically by deferring the application JavaScript bundle and further optimising the video files. For now, though, we should be happy with a 50% improvement in render performance!</p>
<h2 id="optimisation-5-don-t-request-video-on-low-bandwidth-connections" tabindex="-1">Optimisation 5: Don't request video on low-bandwidth connections <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#optimisation-5-don-t-request-video-on-low-bandwidth-connections" aria-hidden="true">#</a></h2>
<p>Another consideration is for users who are on very poor connections, they probably would prefer to not have the video load and instead just the poster image.</p>
<p>We can extend our example to use <code>navigator.connection.downlink</code> to simply toggle the video src based on the connection speed.</p>
<pre class="language-js"><code class="language-js"> <span class="token keyword">const</span> <span class="token constant">MIN_DOWNLINK</span> <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token comment">// Slow 3G ~~ 0.4, Fast 3G ~~ 1.4</span><br /> <span class="token comment">/* ... */</span><br /> <span class="token keyword">let</span> downlink <span class="token operator">=</span> <span class="token constant">MIN_DOWNLINK</span><span class="token punctuation">;</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> downlink <span class="token operator">=</span> navigator<span class="token punctuation">.</span>connection<span class="token punctuation">.</span>downlink<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Unable to determine downlink</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>videoEl<span class="token punctuation">.</span>src <span class="token operator">!==</span> src<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token comment">// only override values if they differ</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>downlink <span class="token operator">>=</span> <span class="token constant">MIN_DOWNLINK</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> videoEl<span class="token punctuation">.</span>src <span class="token operator">=</span> src<span class="token punctuation">;</span><br /> widthDisplay <span class="token operator">+=</span> <span class="token string">" - FAST"</span><span class="token punctuation">;</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Detected bandwidth (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>downlink<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">Mbps) greater than threshold (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MIN_DOWNLINK</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">Mbps) - showing video</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br /> widthDisplay <span class="token operator">+=</span> <span class="token string">" - SLOW"</span><span class="token punctuation">;</span><br /> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Detected bandwidth (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>downlink<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">Mbps) less than threshold (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MIN_DOWNLINK</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">Mbps) - not showing video</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> videoEl<span class="token punctuation">.</span>poster <span class="token operator">=</span> poster<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span></code></pre>
<p>You could also add a play button in this context, so users could opt-in to the video if they would like to see it.</p>
<h2 id="conclusion" tabindex="-1">Conclusion <a class="direct-link" href="https://simonhearne.com/2021/fast-responsive-videos/#conclusion" aria-hidden="true">#</a></h2>
<p>We have applied four simple optimisations to a landing page hero video, removing a layout shift and improving the render time for the video from six seconds to three on a 3G connection. Videos don't have to slow down your site, as long as we pay attention to the details.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fast-responsive-videos/all-tests.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/bsnr4wndR7-600.avif 600w, https://simonhearne.com/img/bsnr4wndR7-900.avif 900w, https://simonhearne.com/img/bsnr4wndR7-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/bsnr4wndR7-600.webp 600w, https://simonhearne.com/img/bsnr4wndR7-900.webp 900w, https://simonhearne.com/img/bsnr4wndR7-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/bsnr4wndR7-600.jpeg 600w, https://simonhearne.com/img/bsnr4wndR7-900.jpeg 900w, https://simonhearne.com/img/bsnr4wndR7-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/bsnr4wndR7-600.jpeg" width="1200" height="1557" /></picture></a><figcaption>Implementing all of these recommendations makes for a much faster user experience (<a href="https://www.webpagetest.org/video/compare.php?tests=211029_AiDc47_80cb876c7d79b44040a1c327d8e366b2-r%3A3%2C211029_AiDcR8_e2e6aa2957e48f3c24d48af9f0085134-r%3A2%2C211029_AiDc6T_94eca459d8cd75434e40dafea045fdb8-r%3A3%2C211102_BiDcPQ_7bab0642d544142f452426a2acc45d08%2C211102_AiDc71_ad06bcf1cf10030ff198aab10ff393b7&thumbSize=150&ival=500&end=visual">view filmstrip</a>)</figcaption></figure>
<p>There are five key steps to ensuring your responsive hero videos render quickly:</p>
<ol>
<li>Set the video <code>src</code> as soon as possible</li>
<li>Use CSS to allocate the correct space for the video</li>
<li>Use the <code>poster</code> attribute for a fast rendering first frame</li>
<li>Use responsive preloads to optimise poster image delivery</li>
<li>Optimize your video assets</li>
</ol>
<p>We didn't cover it in this post, but it is critical that your video assets are optimised! Some simple tricks are to remove the audio track from muted videos, scale the video to the size it will be rendered and use the optimal format for the vistor's browser. Most of these are relatively simple to do, but for responsive videos it can be difficult to determine the optimal widths and formats. Here I would recommend using a video hosting & optimisation service which can automate these tasks:</p>
<ul>
<li><a href="https://www.akamai.com/products/image-and-video-manager">Akamai Image and Video Manager</a></li>
<li><a href="https://cloudinary.com/products/media_optimizer">Cloudinary Media Optimizer</a></li>
<li><a href="https://www.cloudflare.com/products/cloudflare-stream/">Cloudflare Stream</a></li>
</ul>
<p>Each of these (and a number of other services) can automatically serve the best format and width for each visitor, dramatically reducing file size and improving render performance.</p>
<p>To get started with your hero video, you can view the full working demo below or <a href="https://codepen.io/simonhearne/pen/abyVGRj?editors=1100">directly on CodePen</a>. This code should be enhanced with error handling and support for browsers with JavaScript disabled: use a <code><noscript></code> element to contain a fallback video behaviour - perhaps just showing the poster image.</p>
<p class="codepen" data-height="500" data-default-tab="html,result" data-slug-hash="rNdNypB" data-user="simonhearne" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/simonhearne/pen/rNdNypB">
fast-responsive-video</a> by Simon Hearne (<a href="https://codepen.io/simonhearne">@simonhearne</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script defer="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
An Inclusive Web is Fast by Default2021-09-23T00:00:00Zhttps://simonhearne.com/2021/inclusive-design/<p><lite-youtube videoid="MiXy2x6flww" playlabel="Play: An Inclusive Web is Fast by Default"></lite-youtube></p>
<p><a class="link-button no-link" href="https://simonhearne.com/presentations/inclusive-design/#/">View Slides</a></p>
Slow site speed is still the biggest cause of web stress2021-02-26T00:00:00Zhttps://simonhearne.com/2021/web-stress/<p>Back in 2010 Foviance ran an EEG study <a class="citation" id="citation-1" href="https://simonhearne.com/2021/web-stress/#source-1">[1]</a> which showed that participants had to concentrate 50% harder when websites were slow, as measured by alpha waves.</p>
<p>In 2011 Harris International conducted a study for Tealeaf <a class="citation" id="citation-2" href="https://simonhearne.com/2021/web-stress/#source-2">[2]</a> which found that American respondents got more frustrated by failed mobile transactions (58%) than being stuck in traffic (56%). 35% of respondents had cursed at their phone, screamed at it or even thrown it in anger.</p>
<p>In 2014 Radware conducted an EEG study <a class="citation" id="citation-3" href="https://simonhearne.com/2021/web-stress/#source-3">[3]</a> which showed that a 500ms delay resulted in up to 26% increase in stress levels, as measured by alpha waves.</p>
<p>In 2018 Philip Tellis showed that pages which were not interactive soon after they rendered (30% after they were visually ready) triggered the greatest number of 'rage clicks' from users <a class="citation" id="citation-4" href="https://simonhearne.com/2021/web-stress/#source-4">[4]</a>. Rage clicks indicate that user is frustrated that a page is not responding quickly, causing them to click or tap on an element repeatedly in quick succession.</p>
<p>More recently in 2020 <a href="https://www.digitalinformationworld.com/2020/12/researchers-conduct-study-on-how-bad.html">Cyber-Duck</a> tracked users' blood pressure whilst navigating sites <a class="citation" id="citation-5" href="https://simonhearne.com/2021/web-stress/#source-5">[5]</a>. The results showed that slow pages caused the greatest increase in systolic blood pressure (an average 21% increase) when compared with other irritations such as popups and auto-playing music.</p>
<p>It's clear that slow pages increase stress, and we know that stress is not the response we are hoping to induce in our visitors! Delivering fast experiences will reduce visitor stress and might even create user delight.</p>
<p>You can track proxies of stress on your pages through metrics like rage clicks, but ultimately the outcome of slow pages will be lower traffic, shorter sessions and poorer business results. <a href="https://simonhearne.com/2020/core-web-vitals/">Core Web Vitals</a> are the best proxies we have for user experience on the web, so optimising for these should reduce visitor stress and lead to improved business metrics.</p>
<h2 id="sources" tabindex="-1">Sources <a class="direct-link" href="https://simonhearne.com/2021/web-stress/#sources" aria-hidden="true">#</a></h2>
<ol class="sources-list">
<li id="source-1">
<a target="_blank" rel="noopener" href="https://simonhearne.com/files/final_webstress_survey_report_229296.pdf">[PDF] Web Stress. Foviance. 2010.</a> <a class="citation-link" href="https://simonhearne.com/2021/web-stress/#citation-1">(back to text)</a>
</li>
<li id="source-2">
<a target="_blank" rel="noopener" href="http://cdn2.hubspot.net/hub/190519/file-1073485563-pdf/docs/10_-_whitepaper_-_improving_the_customer_experience_for_mobile_consumers.pdf">[PDF] A report on the Mobile Customer Experience. IBM. 2011.</a> <a class="citation-link" href="https://simonhearne.com/2021/web-stress/#citation-2">(back to text)</a>
</li>
<li id="source-3">
<a target="_blank" rel="noopener" href="https://blog.radware.com/applicationdelivery/wpo/2014/06/mobile-web-performance-stress/">Mobile Web Stress: Understanding the Neurological Impact of Poor Performance - Tammy Everts. 2014.</a> <a class="citation-link" href="https://simonhearne.com/2021/web-stress/#citation-3">(back to text)</a>
</li>
<li id="source-4">
<a target="_blank" rel="noopener" href="https://speakerdeck.com/bluesmoon/ux-and-performance-metrics-that-matter-a062d37f-e6c7-4b8a-8399-472ec76bb75e?slide=16">UX & Performance: Metrics that Matter (slide 16). Philip Tellis. 2018.</a> <a class="citation-link" href="https://simonhearne.com/2021/web-stress/#citation-4">(back to text)</a>
</li>
<li id="source-5">
<a target="_blank" rel="noopener" href="https://www.netimperative.com/2020/12/09/blood-pressure-study-which-website-issue-cause-users-the-most-stress/">Blood pressure study: Which website issue cause users the most stress? Net Imperative. 2020.</a> <a class="citation-link" href="https://simonhearne.com/2021/web-stress/#citation-5">(back to text)</a>
</li>
</ol>
Everything we know about Core Web Vitals and SEO2021-02-22T15:30:00Zhttps://simonhearne.com/2021/core-web-vitals-seo/<blockquote class="info"><p><em>June 15th 2021:</em> Multiple updates from <a href="https://events.google.com/io/program/content?lng=en">Google I/O 2021</a>, a <a href="https://www.youtube.com/watch?v=HWm6WNkHs90">Web Vitals Q&A session</a> and another <a href="https://developers.google.com/search/blog/2021/04/more-details-page-experience?hl=en">Google blog post</a>.</p></blockquote>
<blockquote class="info"><p><em>April 19th 2021:</em> Google have <a target="_blank" rel="noopener" href="https://developers.google.com/search/blog/2021/04/more-details-page-experience">announced a delay</a> to the rollout of the Page Experience update. The SEO changes will now gradually roll out from mid-June 2021.</p></blockquote>
<blockquote class="info"><p><em>April 13th 2021:</em> Google have <a target="_blank" rel="noopener" href="https://web.dev/evolving-cls/">updated the CLS metric</a> to better account for complex web applications. This should generally result in a slight improvement in reported CLS values.</p></blockquote>
<blockquote class="info"><p><em>March 30th 2021:</em> Google have updated their <a target="_blank" rel="noopener" href="https://support.google.com/webmasters/thread/104436075">FAQs page</a> to confirm and clarify many of the details in this post!</p></blockquote>
<h2 id="in-summary" tabindex="-1">In summary <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#in-summary" aria-hidden="true">#</a></h2>
<p>The <a href="https://developers.google.com/search/blog/2020/11/timing-for-page-experience#:~:text=Today%20we're%20announcing%20that,roll%20out%20in%20May%202021.&text=The%20change%20for%20non%2DAMP,roll%20out%20in%20May%202021.">update</a> will roll out between July and August 2021, it will result in a positive ranking signal boost for page experience in <strong>mobile search only</strong> <a class="citation" id="citation-1" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-1">[1]</a>, for pages or <strong>groups of similar URLs</strong> <a class="citation" id="citation-2" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-2">[2]</a>, which perform well against the core web vitals targets. The impact of the signal is expected to be small <a class="citation" id="citation-4" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-4">[4]</a>. Google has stated that the Page Experience update will roll out to desktop search at some point in the future.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals-seo/cwv-summary.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/yV52xJ3RxW-600.avif 600w, https://simonhearne.com/img/yV52xJ3RxW-900.avif 900w, https://simonhearne.com/img/yV52xJ3RxW-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/yV52xJ3RxW-600.webp 600w, https://simonhearne.com/img/yV52xJ3RxW-900.webp 900w, https://simonhearne.com/img/yV52xJ3RxW-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/yV52xJ3RxW-600.jpeg 600w, https://simonhearne.com/img/yV52xJ3RxW-900.jpeg 900w, https://simonhearne.com/img/yV52xJ3RxW-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/yV52xJ3RxW-600.jpeg" width="1200" height="592" /></picture></a><figcaption>@fabkru's <a href="https://twitter.com/fabkru/status/1396331280585928707">summary</a> of a <a href="https://www.youtube.com/watch?v=HWm6WNkHs90">Google Q&A session</a> on web vitals.</figcaption></figure>
<p>"Meeting the targets" means that the aggregate 75th percentile value, for a single URL (or across a 'similar' set of URLs when there is not enough data for an individual URL), meets the thresholds for 'good' shown below (2.5s for <abbr title="Largest Contentful Paint">LCP</abbr>, 100ms for <abbr title="First Input Delay">FID</abbr> and 0.1 for <abbr title="Cumulative Layout Shift">CLS</abbr>). The ranking boost is calculated separately for each metric: with green giving the maximum boost, red giving no boost and amber giving some boost <a class="citation" id="citation-3" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-3">[3]</a>. The data is aggregated from Google's private view of <a href="https://developers.google.com/web/tools/chrome-user-experience-report">CrUX</a> data (see <a href="https://simonhearne.com/2021/core-web-vitals-seo/#search-console-is-the-source-of-truth">Search console is the source of truth</a> for details of this assumption).</p>
<div class="resp-cols-3">
<img src="https://simonhearne.com/images/core-web-vitals/lcp_ux.svg" width="171" height="150" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/fid_ux.svg" width="171" height="150" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/cls_ux.svg" width="171" height="150" loading="lazy" />
</div>
<h2 id="urls-can-be-grouped-by-similarity" tabindex="-1">URLs can be grouped by similarity <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#urls-can-be-grouped-by-similarity" aria-hidden="true">#</a></h2>
<p>As can be seen in Search Console, URLs are grouped by Google into similar sets of pages. The aggregate Core Web Vital scores shown here will likely be the ones which impact the Page Experience ranking factor, rather than individual URL performance. You can click on the 'Similar URLs' column in the view below to see up to 20 of the URLs Google has included in the group.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals-seo/cls-issues.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/tqFt9EF0aE-600.avif 600w, https://simonhearne.com/img/tqFt9EF0aE-900.avif 900w, https://simonhearne.com/img/tqFt9EF0aE-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/tqFt9EF0aE-600.webp 600w, https://simonhearne.com/img/tqFt9EF0aE-900.webp 900w, https://simonhearne.com/img/tqFt9EF0aE-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/tqFt9EF0aE-600.jpeg 600w, https://simonhearne.com/img/tqFt9EF0aE-900.jpeg 900w, https://simonhearne.com/img/tqFt9EF0aE-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/tqFt9EF0aE-600.jpeg" width="1200" height="588" /></picture></a><figcaption>Google Search Console CLS Issues report shows URL grouping</figcaption></figure>
<blockquote class="info"><p><em>June 15th 2021:</em> I now believe that URL groups are only used when there is not enough field data for a single URL. Run your page through <a href="https://developers.google.com/speed/pagespeed/insights/">Pagespeed Insights</a> to see if there is enough data for your URL(s).</p></blockquote>
<h2 id="field-is-more-important-than-lab" tabindex="-1">Field is more important than Lab <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#field-is-more-important-than-lab" aria-hidden="true">#</a></h2>
<p>Field data - data collected from real users browsing pages - is the only source which will impact Page Experience <a class="citation" id="citation-5" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-5">[5]</a>. Lab data - test data collected from emulated browsers - may not reflect the real user experience and has no bearing on SEO.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals-seo/origin-lab.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/jMEVPUbLOl-600.avif 600w, https://simonhearne.com/img/jMEVPUbLOl-900.avif 900w, https://simonhearne.com/img/jMEVPUbLOl-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/jMEVPUbLOl-600.webp 600w, https://simonhearne.com/img/jMEVPUbLOl-900.webp 900w, https://simonhearne.com/img/jMEVPUbLOl-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/jMEVPUbLOl-600.jpeg 600w, https://simonhearne.com/img/jMEVPUbLOl-900.jpeg 900w, https://simonhearne.com/img/jMEVPUbLOl-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/jMEVPUbLOl-600.jpeg" width="1200" height="759" /></picture></a><figcaption>Field and Lab data often differ significantly</figcaption></figure>
<p>Any difference between lab and field results could be down to many factors relating to your traffic:</p>
<ul>
<li>Geographic user distribution and varying network quality (LCP)</li>
<li>Varying layouts on different viewports (CLS)</li>
<li>Varying mobile device specifications (FID)</li>
</ul>
<p>Lab data is still useful when diagnosing an issue and confirming an improvement, but we must rely on field data for feedback on the actual user experience delivered. Note that the Lighthouse Score is calculated from lab data, not field! <a class="citation" id="citation-6" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-6">[6]</a></p>
<h2 id="search-console-is-the-source-of-truth" tabindex="-1">Search Console is the source of truth <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#search-console-is-the-source-of-truth" aria-hidden="true">#</a></h2>
<p>Whilst Core Web Vitals values are available in a multitude of tools...</p>
<ul>
<li><a href="https://developers.google.com/speed/pagespeed/insights/">Pagespeed Insights</a> (lab and field)</li>
<li><a href="https://webpagetest.org/">WebPageTest</a> (lab)</li>
<li>Real User Measurement tools like <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">mPulse</a> (field)</li>
<li><a href="https://web.dev/chrome-ux-report-bigquery/">Chrome UX Report</a> (field)</li>
</ul>
<p>...none of these directly reflect the values that will be used by search.</p>
<p>Pagespeed Insights' field data for a page is the closest you can get to the data used in search, outside of Search Console. Pagespeed Insights has the only public source of data at the page level <strong>and</strong> using the same 28-day rolling window as search - but the values shown are for the individual URL under test, not the aggregate of related pages as indicated in Search Console.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals-seo/search-console-2.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/SwifEDCWHu-600.avif 600w, https://simonhearne.com/img/SwifEDCWHu-900.avif 900w, https://simonhearne.com/img/SwifEDCWHu-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/SwifEDCWHu-600.webp 600w, https://simonhearne.com/img/SwifEDCWHu-900.webp 900w, https://simonhearne.com/img/SwifEDCWHu-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/SwifEDCWHu-600.jpeg 600w, https://simonhearne.com/img/SwifEDCWHu-900.jpeg 900w, https://simonhearne.com/img/SwifEDCWHu-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/SwifEDCWHu-600.jpeg" width="1200" height="979" /></picture></a><figcaption>Google Search Console shows page groups which fail Core Web Vitals audits. Data is sourced from CrUX.</figcaption></figure>
<h2 id="what-we-need-to-act-upon-now" tabindex="-1">What we need to act upon now <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#what-we-need-to-act-upon-now" aria-hidden="true">#</a></h2>
<p>The Page Experience Update is more of a carrot approach than stick - there is no direct penalty for failing to meet Google's goals. That said, meeting the target values for Core Web Vitals will inevitably result in an improved user experience. Use the tools available to determine which pages on your sites are falling behind, then get to work on improving them! I've written a post on <a href="https://simonhearne.com/2020/core-web-vitals/">diagnosing and improving Core Web Vitals</a> which may help.</p>
<p>Remember that any changes you make to your page performance will take 28 days to fully reflect in Google's data - so don't expect immediate returns there.</p>
<h2 id="do-we-like-core-web-vitals" tabindex="-1">Do we like Core Web Vitals? <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#do-we-like-core-web-vitals" aria-hidden="true">#</a></h2>
<p>I believe that Core Web Vitals are on balance a Good Thing™️:</p>
<ul>
<li>The vitals encourage us to focus on UX metrics, not just technical metrics</li>
<li>The update builds a bridge between SEO, Tech SEO, Performance Marketing and Engineering</li>
<li>Using field data increases focus on real user measurement, versus the more simple (and less representative) lab testing</li>
<li>The targets start a good discussion around percentiles for field data - 75th is a good balance between the median (hard to impact) and the 90th / 95th (potentially subject to volatility)</li>
</ul>
<p>There are a number of issues with Core Web Vitals though:</p>
<ul>
<li>The metrics are only measurable in Chromium-based browsers</li>
<li>Ranking benefit is only determined by performance of visitors on Chrome browsers</li>
<li>First Input Delay depends heavily on when (and if!) users interact with a page, making it less actionable than Total Blocking Time or maximum potential FID <a class="citation" id="citation-7" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-7">[7]</a>.</li>
</ul>
<p>It's worth noting at this point that the targets for Core Web Vitals set out by Google are quite strict. Many Google pages do not achieve 'good' in all three, and very few production websites do. That said, it is possible to achieve green across-the-board without significant engineering work. In my opinion this makes them good targets: difficult but achievable.</p>
<h2 id="other-seo-factors" tabindex="-1">Other SEO factors <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#other-seo-factors" aria-hidden="true">#</a></h2>
<p>AMP will no longer be a requirement for the Top Stories carousel in mobile search after the update, meaning all pages which meet the Google News content policies will be eligible for inclusion, with Core Web Vitals impacting ranking in a similar way to normal search. <a class="citation" id="citation-8" href="https://simonhearne.com/2021/core-web-vitals-seo/#source-8">[8]</a></p>
<h2 id="what-is-yet-to-be-confirmed" tabindex="-1">What is yet to be confirmed? <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#what-is-yet-to-be-confirmed" aria-hidden="true">#</a></h2>
<p>There are still a number of assumptions we are making, which are yet to be confirmed by Google:</p>
<ul>
<li>URLs are grouped for the purposes of the ranking signal, and origin summary used for new / ungrouped URLs. Can we control this grouping? It sometimes does not follow seemingly logical patterns (e.g. PDP and PLPs grouped together)</li>
<li>How many experiences are included in the CrUX dataset - e.g. how many users opt-in to share usage statistics and what (if any) sampling is applied</li>
<li>Is geography a factor, e.g. does the TTFB measured in a country determine page experience ranking in that country? (June 15th 2021: I believe geography is not a factor. Core Web Vitals are measured by country, but the ranking boost is global).</li>
<li>CLS unfairly penalises single-page applications, there is a <a href="https://web.dev/better-layout-shift-metric/">proposal</a> to improve the metric but we do not know if this will be released before May. (June 15th 2021: Google has <a href="https://web.dev/evolving-cls/">confirmed</a> that they are using the new CLS metric - calculated by windows of up to five seconds throughout a page lifecycle. This is better for SPAs than before, but does not fix the fundamental issue: soft navigations / route changes are not accounted for correctly.)</li>
</ul>
<p>I hope that these points are confirmed before the May 31st deadline, if nothing else but for clarity and transparency. It's hard to hit a target that you cannot see!</p>
<h2 id="am-i-missing-something" tabindex="-1">Am I missing something? <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#am-i-missing-something" aria-hidden="true">#</a></h2>
<p>Probably! I have collected this information from videos, blog posts and tweets; trying to find a legitimate source of truth is disappointingly difficult. If you know of something different to the statements above (with a source) please let me know by email or twitter - I'm keen to update this page as we learn more.</p>
<h2 id="sources" tabindex="-1">Sources <a class="direct-link" href="https://simonhearne.com/2021/core-web-vitals-seo/#sources" aria-hidden="true">#</a></h2>
<ol class="sources-list">
<li id="source-1">
"At this time, using page experience as a signal for ranking will apply only to mobile Search" - <a target="_blank" rel="noopener" href="https://support.google.com/webmasters/thread/86521401?hl=en#gbwa:~:text=Q%3A%20Why%20are%20there%20differences%20in%20scores%20between%20mobile%20and%20desktop%3F">Google Search Console Help. 12 March 2020.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-1">(back to text)</a>
</li>
<li id="source-2">
<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?t=848&v=JV7egfF29pI&feature=youtu.be">John Mueller in Google SEO office-hours on URL grouping</a>: <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-2">(back to text)</a>
<blockquote>"... for example, to split out the forum from our site where we can tell, oh, slash forum is everything forum and it's kind of slow, and everything else that's not in slash forum is really fast. If we can recognize that fairly easily, that's a lot easier. Then we can really say, <em>everything here in slash forum is kind of slow</em>, everything here is kind of OK. On the other hand, if we have to do this on a per URL basis where ... we can't tell based on the URL if this is a part of the forum or part of the rest of your site, then we can't really group that into parts of your website. And then we'll be forced to take an aggregate score across your whole site and apply that appropriately. I suspect we'll have a little bit more information on this as we get closer to announcing or closer to the date when we start using Core Web Vitals in search, but it is something you can look at already a little bit in Search Console. There is a Core Web Vitals report there, and if you drill down to Individual Issues, you'll also see this your URL affects so many similar URLs. And based on that, you can already kind of tell, oh, is Google able to figure out that my forum is grouped together or is it not able to figure out that these belong together?"</blockquote>
</li>
<li id="source-3">
<a target="_blank" rel="noopener" href="https://twitter.com/JohnMu/status/1395798952570724352">@JohnMu on Twitter.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-3">(back to text)</a>
</li>
<li id="source-4">
"Ranking wise it's a teeny tiny factor, very similar to https ranking boost." (not directly related to Page Experience, but can we infer similarity) - <a target="_blank" rel="noopener" href="https://twitter.com/methode/status/1255224116648476675">Gary Illyes (Google's Webmaster Trends Analyst). 28 April 2020.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-4">(back to text)</a>
</li>
<li id="source-5">
"Core Web Vitals are a set of <em>real-world</em>, user-centered metrics" - <a target="_blank" rel="noopener" href="https://developers.google.com/search/blog/2020/05/evaluating-page-experience#about-page-experience:~:text=Core%20Web%20Vitals%20are%20a%20set%20of%20real%2Dworld%2C%20user%2Dcentered%20metrics">Google Search Central. 28 May 2020.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-5">(back to text)</a>
</li>
<li id="source-6">
"The Performance score is a weighted average of the <em>metric</em> scores." - <a target="_blank" rel="noopener" href="https://web.dev/performance-scoring/">Lighthouse performance scoring. 12 June 2020.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-6">(back to text)</a>
</li>
<li id="source-7">
"Since FID measures when actual users first interact with your page, it's more inherently variable than typical performance metrics. See Analyzing and reporting on FID data for guidance about how to evaluate the FID data you collect." - <a target="_blank" rel="noopener" href="https://web.dev/lighthouse-max-potential-fid/">Max Potential First Input Delay. 16 October 2019.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-7">(back to text)</a>
</li>
<li id="source-8">
"As part of this update, we'll ... remove the AMP requirement from Top Stories eligibility" - <a target="_blank" rel="noopener" href="https://developers.google.com/search/blog/2020/05/evaluating-page-experience#devsite-thumb-label-header:~:text=incorporate%20the%20page%20experience%20metrics%20into,AMP%20requirement%20from%20Top%20Stories%20eligibility">Evaluating page experience for a better web. 28 May 2020.</a> <a href="https://simonhearne.com/2021/core-web-vitals-seo/#citation-8">(back to text)</a>
</li>
</ol>
How to avoid layout shifts caused by web fonts2021-01-19T15:30:00Zhttps://simonhearne.com/2021/layout-shifts-webfonts/<p>One of the outcomes of the release of <a href="https://simonhearne.com/2020/core-web-vitals/">Core Web Vitals</a> (and subsequent <a href="https://developers.google.com/search/blog/2020/11/timing-for-page-experience">inclusion</a> in Google's page ranking algorithm) is that we have been paying more attention to unexpected layout shifts and the cumulative layout shift (CLS) score.</p>
<p>Some sources of layout shifts have been simple to resolve: pre-allocate the correct space for dynamic elements, use width and height attributes on images and prioritise visible elements in your HTML document. One common cause of layout shift is surprisingly difficult to resolve though: flashes of unstyled text (FOUT).</p>
<p>In this post we will explore the surprisingly complex world of text rendering on the web and some techniques to remove FOUT while not incurring a <abbr title="Cumulative Layout Shift">CLS</abbr> penalty.</p>
<p>In summary we need to prevent the layout shift by letting the browser render in a fallback system font if it doesn't get the web font in time, then optimise our fonts to try to get them to the browser before it needs them:</p>
<ul>
<li>use <code>font-display: optional</code> to prevent layout shifts</li>
<li>subset fonts and serve as <code>woff2</code></li>
<li>use variable fonts or a limited set of weight variations</li>
<li>preload critical fonts</li>
<li>host your own fonts on your main domain</li>
</ul>
<h2 id="why-fonts-cause-layout-shifts" tabindex="-1">Why fonts cause layout shifts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#why-fonts-cause-layout-shifts" aria-hidden="true">#</a></h2>
<p>Unexpected layout shifts (page content moving around without user interaction) are bad for user experience. Fonts cause layout shifts when the size of the containing element (e.g. a <code><div></code> or paragraph) changes when the web font is downloaded. This occurs when the height of the font or the length of the paragraph is different with the web font compared to the system font. When laying out a page, browsers will use the dimensions and properties of the fallback font to determine the size of the containing elements, even if you have declared a web font to block the system font with <code>font-display: block</code>!</p>
<style>
#demo-text-1 {
overflow: hidden;
width: 80%;
max-width: 500px;
font-size: smaller;
border: 1px solid grey;
}
.no-font {
font-family: serif !important;
border-color: red !important;
}
.fout-fig {
height: 220px;
position:relative;
}
@media only screen and (max-width: 600px) {
.fout-fig {
height: 350px;
}
}
</style>
<figure class="fout-fig">
<p id="demo-text-1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin aliquam metus eu lacus placerat feugiat. Suspendisse quis elit fringilla, luctus felis ut, ultricies leo. Morbi vel dui non risus aliquam rhoncus in ac justo. Mauris cursus diam id ipsum aliquet tempus. Praesent feugiat consequat risus, ut eleifend neque consequat quis. Maecenas rhoncus faucibus dui quis pretium. Nam ultrices rhoncus dui, at pretium erat rhoncus et. Vestibulum tempus diam vel ex venenatis placerat. Nam sed elementum odio. Suspendisse vel orci turpis. Fusce in maximus ante, non malesuada justo. Duis in tellus erat.</p>
<figcaption style="position: absolute; width:100%; bottom:0;">Flash of Unstyled Text</figcaption>
</figure>
<script>
window.setInterval(() => {
document.getElementById('demo-text-1').classList.toggle("no-font");
},2000);
</script>
<h3 id="detecting-layout-shifts" tabindex="-1">Detecting layout shifts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#detecting-layout-shifts" aria-hidden="true">#</a></h3>
<p>There are a number of methods to detect layout shifts due to web fonts, the most simple is to run your page through <a href="https://webpagetest.org/">WebPageTest</a> and use the filmstrip view. There is a toggle to show layout shifts, combine that with 'huge' screenshot images and a 0.1s interval to get a clear view of what is happening as your page renders (you can simply add this to the end of the URL: <code>&highlightCLS=1&thumbSize=600&ival=100&end=visual&text=000&bg=fff"</code>). The Washington Post has a couple of layout shifts due to web fonts, in the example below the headline text is one line shorter with the web font than without which results in a layout shift. Layout shifts are dependent on viewport size so make sure you test a few different devices.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/wapo-headline.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/wk21C2FubT-600.avif 600w, https://simonhearne.com/img/wk21C2FubT-900.avif 900w, https://simonhearne.com/img/wk21C2FubT-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/wk21C2FubT-600.webp 600w, https://simonhearne.com/img/wk21C2FubT-900.webp 900w, https://simonhearne.com/img/wk21C2FubT-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/wk21C2FubT-600.jpeg 600w, https://simonhearne.com/img/wk21C2FubT-900.jpeg 900w, https://simonhearne.com/img/wk21C2FubT-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/wk21C2FubT-600.jpeg" width="1200" height="442" /></picture></a><figcaption>Headline web font loading causes a layout shift. <a href="https://webpagetest.org/video/compare.php?tests=210120_Di3C_126898f84daca6ada62ee7736a29bddb-r%3A1-c%3A0&highlightCLS=1&thumbSize=600&ival=100&end=visual&text=000&bg=fff">View test on WPT</a></figcaption></figure>
<p>You can also find layout shifts in the Chrome Developer Tools Performance tab, look for layout shifts attributed to text elements.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/wapo-shift.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/r-rbEQjqHA-600.avif 600w, https://simonhearne.com/img/r-rbEQjqHA-900.avif 900w, https://simonhearne.com/img/r-rbEQjqHA-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/r-rbEQjqHA-600.webp 600w, https://simonhearne.com/img/r-rbEQjqHA-900.webp 900w, https://simonhearne.com/img/r-rbEQjqHA-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/r-rbEQjqHA-600.jpeg 600w, https://simonhearne.com/img/r-rbEQjqHA-900.jpeg 900w, https://simonhearne.com/img/r-rbEQjqHA-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/r-rbEQjqHA-600.jpeg" width="1200" height="688" /></picture></a><figcaption>Chrome developer tools shows attribution of layout shifts</figcaption></figure>
<p>I've covered general layout shifts in more detail in a post on <a href="https://simonhearne.com/2020/core-web-vitals/">How to Improve Core Web Vitals</a>.</p>
<h2 id="prevent-layout-shifts-with-font-display" tabindex="-1">Prevent layout shifts with font-display <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#prevent-layout-shifts-with-font-display" aria-hidden="true">#</a></h2>
<p>The dirty little secret of this blog post is that you can resolve layout shifts due to fonts with a single line of CSS:</p>
<p><code>font-display: optional</code></p>
<p>This directive lives in your font-face declaration and tells the browser to use a fallback system font if the web font is not available at the time of rendering text (plus 100ms). This means that on uncached page loads there is a chance that the fallback font will be used, but all subsequent page loads should be rendered with the web font, as it will be downloaded and available in cache. There are other options, shown in the image below:</p>
<ul>
<li><strong>block</strong> - hide text for up to three seconds while waiting for the web font, and always swap in the web font when it loads</li>
<li><strong>swap</strong> - show text as soon as possible, and always swap in the web font when it loads</li>
<li><strong>fallback</strong> - hide text for up to 100ms, then only swap in the web font if it loads within three seconds</li>
<li><strong>optional</strong> - hide text for up to 100ms, then only use the web font if it is available - never swapping</li>
</ul>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/font-display.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/wxg5AYO-R--600.avif 600w, https://simonhearne.com/img/wxg5AYO-R--900.avif 900w, https://simonhearne.com/img/wxg5AYO-R--1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/wxg5AYO-R--600.webp 600w, https://simonhearne.com/img/wxg5AYO-R--900.webp 900w, https://simonhearne.com/img/wxg5AYO-R--1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/wxg5AYO-R--600.jpeg 600w, https://simonhearne.com/img/wxg5AYO-R--900.jpeg 900w, https://simonhearne.com/img/wxg5AYO-R--1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/wxg5AYO-R--600.jpeg" width="1200" height="674" /></picture></a><figcaption>Optional is the only font-display value which guarantees no layout shift. <a href="https://speakerdeck.com/notwaldorf/fontastic-web-performance?slide=74">Image</a> by <a href="https://twitter.com/notwaldorf">@notwalforf</a>. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display">Read more on MDN</a></figcaption></figure>
<p>If it's that simple, why this whole blog post? Unfortunately system fonts are not necessarily the nicest designs, and they are not consistent between operating systems. Most designers will cringe at the thought of showing users a fallback system font. The rest of this blog post details various optimisations to get the font files to the browser quicker, allowing the use of any font-display option but with minimal risk of showing a system font, or for options other than <code>optional</code>: without triggering a layout shift. We will also look at font-face descriptors / f-mods and progressive font enrichment for potential further optimisations in the future.</p>
<h2 id="load-fewer-font-files" tabindex="-1">Load fewer font files <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#load-fewer-font-files" aria-hidden="true">#</a></h2>
<p>Downloading one or two font files to render text won't have a massive impact on speed, downloading five or ten font files will! Ensuring that you deliver the minimium viable number of font files is the best way to ensure that the browser has them available at layout, thus reducing the likelihood of <abbr title="Flash of Unstyled Text">FOUT</abbr> layout shifts. Let's look at some tricks to load fewer font files while maintaining your design.</p>
<h3 id="use-faux-bold-and-italic" tabindex="-1">Use faux bold and italic <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#use-faux-bold-and-italic" aria-hidden="true">#</a></h3>
<blockquote>
<p>Designers hate this one weird trick!</p>
</blockquote>
<p>Font stacks generally include a bunch of different files, from extra-light italic through to extra-bold. Combining nine font weights with normal and italic variants produces 18 separate font files! Fonts that are not used on the page will not be downloaded, but for the odd occasion where only a single word is both bold and italic (for example) the whole font file will be downloaded to render the single word.</p>
<p>Browsers can make bold and italic versions of fonts themselves, this is called faux bold and faux italic. This could mean that a single regular weight font file is all you need! This should be tested and approved by any designers involved, I recommend a blind <a href="https://juiceboxinteractive.com/blog/how-pepsi-won-the-battle-but-lost-the-challenge/">Coke vs. Pepsi</a> style test to prevent any bias impacting the outcome.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/faux-bold.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/gYPhuHgMC4-600.avif 600w, https://simonhearne.com/img/gYPhuHgMC4-900.avif 900w, https://simonhearne.com/img/gYPhuHgMC4-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/gYPhuHgMC4-600.webp 600w, https://simonhearne.com/img/gYPhuHgMC4-900.webp 900w, https://simonhearne.com/img/gYPhuHgMC4-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/gYPhuHgMC4-600.jpeg 600w, https://simonhearne.com/img/gYPhuHgMC4-900.jpeg 900w, https://simonhearne.com/img/gYPhuHgMC4-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="image showing faux bold of roboto font compared to real bold, there is little difference" loading="lazy" decoding="async" src="https://simonhearne.com/img/gYPhuHgMC4-600.jpeg" width="1200" height="636" /></picture></a><figcaption>Example of differences between faux and real bold. <a href="https://graphicdesign.stackexchange.com/questions/75965/distinguishing-real-and-faux-bold-and-italics">source</a></figcaption></figure>
<p>There may be subtle differences between the browser's version of a bold font and the font creator's version, as shown above. For some fonts the differences will be too great to consider, especially fancy ones! You can check yours by simply removing the @font-face declarations for all but the regular font version and comparing screenshots of rendered text.</p>
<h3 id="use-a-variable-font" tabindex="-1">Use a variable font <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#use-a-variable-font" aria-hidden="true">#</a></h3>
<p>All modern browsers support variable fonts (notable exceptions are <a href="https://caniuse.com/variable-fonts">IE11 and Opera Mini</a>), this allows a single font file to include multiple variable axes like font weight and slant. This results in a larger file than a single weight font, but it replaces multiple weight files and provides more flexibility for the variable axes (e.g. font-weight: <span style="font-weight: 457">457</span>).</p>
<p>Variable fonts can be found in multiple places, for example Google Fonts <a href="https://fonts.google.com/?vfonly=true">has a filter</a> to only show variable fonts. The majority of variable fonts have a single variable axis for weight, but there are a number of possible dimensions including slant, serif and optical size. <a href="https://fonts.google.com/specimen/Exo?vfonly=true">Exo</a> for example has both weight and a binary italic dimension. <a href="https://www.axis-praxis.org/specimens/__DEFAULT__">Axis Praxis</a> is a great place to explore the possibilities of variable fonts.</p>
<h3 id="death-to-icon-fonts" tabindex="-1">Death to icon fonts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#death-to-icon-fonts" aria-hidden="true">#</a></h3>
<p>Icon font sets like <a href="https://fontawesome.com/">Font Awesome</a> and Bootsrap's <a href="https://getbootstrap.com/docs/3.3/components/">glyphicons</a> have popularised the use of web fonts for iconography. Unfortunately this means that your icons will not render until a (typically) large font file has downloaded, and sometimes results in an unsightly <span style="font-family:sans-serif">⃞</span> symbol instead of your icons when the font file fails to download in time.</p>
<p>Icon fonts are bad practice for performance and accessibility, <a href="https://www.wouterbulten.nl/blog/tech/blog-optimization-replacing-font-awesome-with-svg/">replace them with SVG</a> as soon as possible.</p>
<p>N.B. this section is named after the great talk given by <a href="https://twitter.com/ninjanails">Seren Davies</a>: <a href="https://www.youtube.com/watch?v=9xXBYcWgCHA">Death to Iconfonts ☠️</a></p>
<h3 id="use-system-fonts" tabindex="-1">Use system fonts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#use-system-fonts" aria-hidden="true">#</a></h3>
<p>Web fonts are popular because they allow designers to maintain a consistent look and feel across browsers. Where this isn't necessary, system fonts will be the fastest method to render text. If your current web font is close to a system font, you can use <a href="https://meowni.ca/font-style-matcher/">Font Style Matcher</a> by <a href="https://twitter.com/notwaldorf">Monica</a> to tweak the font settings until you get a near-perfect match. I've worked with a client who surreptitiously replaced their web font stack with a tweaked system font for two weeks - neither designers nor customers apparently noticed the difference!</p>
<p>Using a system font means that text will render at the earliest possible time. We also now have methods to make the font match the operating system, which may be more attractive than previous fallback options like Arial and Helvetica. To do this you'll need to list the system fonts for all operating systems in a specific order:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">body</span> <span class="token punctuation">{</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> -apple-system<span class="token punctuation">,</span> BlinkMacSystemFont<span class="token punctuation">,</span><br /> <span class="token string">"Segoe UI"</span><span class="token punctuation">,</span> <span class="token string">"Roboto"</span><span class="token punctuation">,</span> <span class="token string">"Oxygen"</span><span class="token punctuation">,</span> <span class="token string">"Ubuntu"</span><span class="token punctuation">,</span> <span class="token string">"Cantarell"</span><span class="token punctuation">,</span><br /> <span class="token string">"Fira Sans"</span><span class="token punctuation">,</span> <span class="token string">"Droid Sans"</span><span class="token punctuation">,</span> <span class="token string">"Helvetica Neue"</span><span class="token punctuation">,</span><br /> sans-serif<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>You can read more about this approach in a Smashing Magazine <a href="https://www.smashingmagazine.com/2015/11/using-system-ui-fonts-practical-guide/">article from 2015</a>. One drawback to this approach is that it cannot co-exist with the <a href="https://simonhearne.com/2021/layout-shifts-webfonts/#reduce-layout-shift-with-f-mods">f-mods</a> approach we describe below, this is to be used <em>instead</em> of web fonts. <a href="https://simonhearne.com/2021/layout-shifts-webfonts/#conversation">Let me know</a> if I'm wrong in this assumption!</p>
<h2 id="optimise-font-files" tabindex="-1">Optimise font files <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#optimise-font-files" aria-hidden="true">#</a></h2>
<p>If we must download a font file, we should make it as small as possible to ensure it downloads quickly. There are two key methods to optimise web fonts: subsetting and formats.</p>
<h3 id="subset-fonts" tabindex="-1">Subset fonts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#subset-fonts" aria-hidden="true">#</a></h3>
<p>Many fonts will have glyphs (glyphs are individual characters like <code>a</code> or <code>&</code>) from multiple alphabets. If your website is only delivered in the latin alphabet (a - Z) and does not use ligatures (like <code>é</code>) then these glyphs represent wasted bytes in your font file. Roboto, the most popular Google font, has 277 glyphs.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/roboto-glyphs.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_7UwByYu4L-600.avif 600w, https://simonhearne.com/img/_7UwByYu4L-900.avif 900w, https://simonhearne.com/img/_7UwByYu4L-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_7UwByYu4L-600.webp 600w, https://simonhearne.com/img/_7UwByYu4L-900.webp 900w, https://simonhearne.com/img/_7UwByYu4L-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_7UwByYu4L-600.jpeg 600w, https://simonhearne.com/img/_7UwByYu4L-900.jpeg 900w, https://simonhearne.com/img/_7UwByYu4L-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="image of all glyphs in the roboto font from google, showing many non-latin characters" loading="lazy" decoding="async" src="https://simonhearne.com/img/_7UwByYu4L-600.jpeg" width="1200" height="372" /></picture></a><figcaption>Glyphs in the popular Roboto font. <a href="https://fonts.google.com/specimen/Roboto?selection.family=Roboto&sidebar.open=true#glyphs">source</a></figcaption></figure>
<p>Removing non-latin characters from this font results in a <code>woff2</code> file that is one sixth the size!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/subset-roboto.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/dbjqRi3ibh-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/dbjqRi3ibh-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of file system showing subset roboto font is 11KB, original is 66KB" loading="lazy" decoding="async" src="https://simonhearne.com/img/dbjqRi3ibh-600.jpeg" width="600" height="107" /></picture></a><figcaption>Subset font is 5x smaller</figcaption></figure>
<p>If you use Google fonts, you can <a href="https://developers.google.com/fonts/docs/getting_started#specifying_script_subsets">specify which subsets</a> you require in the CSS request. If you host your own fonts (which I would always recommend, where possible) you will need to check with the font provider if they have subset versions, or where your license permits you can create subsets yourself.</p>
<p>To create subsets you will ideally start with the <code>ttf</code> file and work out what glyphs you need to keep. Filament Group have created <a href="https://github.com/filamentgroup/glyphhanger">glyphhanger</a> to take out the hard work here, read their <a href="https://www.filamentgroup.com/lab/glyphhanger/">blog post</a> to see how easy it is to create a unique subset font just for your pages. Note that missing glyphs will be rendered in the fallback system font, so they won't just disappear!</p>
<p>It is possible to create multiple subset variations of your fonts if your website supports multiple languages. In this case, use the <code>unicode-range</code> declaration in your @font-face rule to let the browser know which characters are in which font file. Read more about this on <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range">MDN</a> or take a look at <a href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap">a response</a> from Google Fonts for inspiration.</p>
<h3 id="use-modern-formats" tabindex="-1">Use modern formats <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#use-modern-formats" aria-hidden="true">#</a></h3>
<p>There are a set of different formats available to serve web fonts: <code>ttf</code>, <code>otf</code>, <code>eot</code>, <code>woff</code> and <code>woff2</code>. <code>ttf</code> and <code>otf</code> are the 'raw' font files, and probably shouldn't be sent to a browser. <code>eot</code> supports subsetting and is compressed using LZ compression, whereas <code>woff</code> uses gzip compression and <code>woff2</code> uses brotli compression.</p>
<p>Font stacks will often have many or all of these formats available, but you really only need <code>woff2</code>. The newer format is around 30% smaller than <code>woff</code> and is supported in all modern browsers. Supporting only <code>woff2</code> will make it more simple to apply some of the other optimisations recommended, such as subsetting.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/woff2-support.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/zWJICqkLpV-600.avif 600w, https://simonhearne.com/img/zWJICqkLpV-900.avif 900w, https://simonhearne.com/img/zWJICqkLpV-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/zWJICqkLpV-600.webp 600w, https://simonhearne.com/img/zWJICqkLpV-900.webp 900w, https://simonhearne.com/img/zWJICqkLpV-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/zWJICqkLpV-600.jpeg 600w, https://simonhearne.com/img/zWJICqkLpV-900.jpeg 900w, https://simonhearne.com/img/zWJICqkLpV-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="can i use dot com shows all browsers except opera and IE11 support woff2" loading="lazy" decoding="async" src="https://simonhearne.com/img/zWJICqkLpV-600.jpeg" width="1200" height="397" /></picture></a><figcaption><code>woff2</code> is supported on all the browsers you care about. <a href="https://caniuse.com/woff2">source</a></figcaption></figure>
<h2 id="deliver-your-fonts-fast" tabindex="-1">Deliver your fonts fast <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#deliver-your-fonts-fast" aria-hidden="true">#</a></h2>
<p>It may seem obvious, but we must make sure that the browser can download our font files as soon as possible! There are a few techniques we should follow to ensure this is always the case. Hosting fonts on your own domain (and ideally on a content delivery network (CDN)) will result in the best performance.</p>
<h3 id="host-your-own-fonts" tabindex="-1">Host your own fonts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#host-your-own-fonts" aria-hidden="true">#</a></h3>
<p>Font foundries such as Google Fonts are popular: the HTTP Archive shows that <a href="https://almanac.httparchive.org/en/2020/fonts#where-are-web-fonts-being-used">80% of sites</a> use web fonts, and <a href="https://almanac.httparchive.org/en/2020/third-parties#third-party-domains">7.5%</a> of all web fonts are served by Google. Using a third-party service can incur a performance penalty, though. The Google Fonts embed process uses either a CSS link or @import, with the CSS file returning a dynamic set of @font-face rules. The rules are distinct for the given font, user agent and any additional query parameters (like subsetting and font-display options).</p>
<p>Using a third-party service means that your fonts will be delayed. The best-case scenario is that you are requesting the font file directly from another hostname (e.g. <a href="http://fonts.gstatic.com/">fonts.gstatic.com</a>) which incurs a connection cost - DNS lookup, TCP connection and TLS negotiation. The worst case is multiple hops, like loading a CSS file from <a href="http://fonts.googleapis.com/">fonts.googleapis.com</a> which references files on <a href="http://fonts.gstatic.com/">fonts.gstatic.com</a>, incurring two connection penalties. This cost may be outweighed by the benefit in some cases for Google Fonts though, as the fonts they deliver are well optimised and subset by alphabet automatically.</p>
<p>In general you should serve the fonts from your domain to avoid the cost of connecting to third-party domains, this will be especially important on high latency connections.</p>
<h3 id="cache-your-fonts" tabindex="-1">Cache your fonts <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#cache-your-fonts" aria-hidden="true">#</a></h3>
<p>Fonts can be cached in two places: the client and the <abbr title="Content Delivery Network">CDN</abbr>. Caching on the client is important for mid-session navigations and should be done in such a way as to avoid revalidation requests. Revalidation requests (<code>if-not-modified</code> and <code>if-modified-since</code>) will block the browser from using the font file until it has verified that it has not changed on the server. Fonts rarely change, so we should implement a cache header as follows and update the filename if the font changes to break the cache:</p>
<p><code>cache-control: max-age=31536000,immutable</code></p>
<p>This tells browsers that they can keep the font for up to a year and that it does not need to be revalidated (<code>immutable</code> <a href="https://caniuse.com/mdn-http_headers_cache-control_immutable">is supported</a> in Firefox and Safari, Chrome should avoid revalidation requests automatically). Avoid adding ETags to these responses as they may force revalidation.</p>
<p>Check also that your Content Delivery Network configuration can store the font files in cache, older configurations may not include the <code>.woff2</code> extension resulting in origin hits and slowing down the response.</p>
<h3 id="use-preload-hints" tabindex="-1">Use preload hints <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#use-preload-hints" aria-hidden="true">#</a></h3>
<p>Web browsers do not download fonts unnecessarily, they wait until the render tree has been constructed in order to know which fonts are required. This means that web fonts are only requested when the browser has downloaded and parsed the HTML and CSS, right before text is rendered. Note that inline CSS does not require a network request - meaning your fonts could be fetched earlier in the page load.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/font-crp.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/YUAv4VLGbe-600.avif 600w, https://simonhearne.com/img/YUAv4VLGbe-900.avif 900w, https://simonhearne.com/img/YUAv4VLGbe-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/YUAv4VLGbe-600.webp 600w, https://simonhearne.com/img/YUAv4VLGbe-900.webp 900w, https://simonhearne.com/img/YUAv4VLGbe-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/YUAv4VLGbe-600.jpeg 600w, https://simonhearne.com/img/YUAv4VLGbe-900.jpeg 900w, https://simonhearne.com/img/YUAv4VLGbe-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="flow diagram shows that web font loading is blocked by render tree construction" loading="lazy" decoding="async" src="https://simonhearne.com/img/YUAv4VLGbe-600.jpeg" width="1200" height="454" /></picture></a><figcaption>Render tree construction delays web font request. <a href="https://web.dev/optimize-webfont-loading/">source</a></figcaption></figure>
<p>If we know that a web font is definitely going to be required to render text on a page, we can make a promise to the browser that it needs to be downloaded as soon as possible. This is a preload hint:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>preload<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/my-font.woff2<span class="token punctuation">"</span></span> <span class="token attr-name">crossorigin</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>anonymous<span class="token punctuation">"</span></span> <span class="token attr-name">as</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>font<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>font/woff2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>When the browser parses this line of HTML it will immediately dispatch a high priority request for the font file. Doing this means that the web font is much more likely to be available when the browser renders text.</p>
<blockquote class="warning"><p>preloaded requests will cannibalise bandwidth from other early requests, use with caution!</p></blockquote>
<p>Preloaded requests are high-priority. In the waterfall section below you can see that five font files are downloaded, one blocks the page CSS (possibly because the <code><link rel="preload"></code> was above the <code><link rel="stylesheet"></code>) and the others block the main JavaScript bundle for the page. Note that this page was served over HTTP/2 on Chrome, other browsers and protocols will vary!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/layout-shifts-webfonts/preload-slow-script.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/GPYhk8icsi-600.avif 600w, https://simonhearne.com/img/GPYhk8icsi-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/GPYhk8icsi-600.webp 600w, https://simonhearne.com/img/GPYhk8icsi-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/GPYhk8icsi-600.jpeg 600w, https://simonhearne.com/img/GPYhk8icsi-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="waterfall section showing fonts blocking main javascript bundle download" loading="lazy" decoding="async" src="https://simonhearne.com/img/GPYhk8icsi-600.jpeg" width="900" height="574" /></picture></a><figcaption>Preloaded fonts delay main JS bundle.</figcaption></figure>
<p>Ensure that your preload tags are below critical content in the head and limit to two or three font files to get the best benefit.</p>
<h2 id="reduce-layout-shift-with-f-mods" tabindex="-1">Reduce layout shift with f-mods <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#reduce-layout-shift-with-f-mods" aria-hidden="true">#</a></h2>
<p>If <code>font-display: optional</code> is not possible for your design, we need to attempt to match the fallback font layout to the web font as closely as possible. Enter font-display modifiers (f-mods), also known as font-face descriptors.</p>
<p>F-mods are in a <a href="https://docs.google.com/document/d/1PW-5ML5hOZw7GczOargelPo6_8Zkuk2DXtgfOtJ59Eo/edit">proposed update</a> to the font-face descriptors specification which includes four new descriptors:</p>
<ul>
<li><strong>ascent-override (%)</strong> - overrides the size allocated for ascenders</li>
<li><strong>descent-override (%)</strong> - overrides the line height allocated for descenders</li>
<li><strong>line-gap-override (%)</strong> - overrides the gap between lines</li>
<li><strong>advance-override (#)</strong> - sets an extra advance for each character, to help match line width and prevent word overflows</li>
</ul>
<p>The first three all impact the height of a line: line box height = ascent + descent + line gap. Baseline position = line box top + line gap / 2 + ascent. For example, if we have <code>ascent-override: 80%; descent-override: 20%; line-gap-override: 0%</code>, then each line box has height 1em (assuming 1em used font size), and the baseline is positioned at 0.8em below the line box top. The <code>advance-override</code> descriptor adds a fraction of the font-size as a gap before each character, for example <code>font-size:16px; advance-override: 0.1;</code> will add a 1.6px gap before each character.</p>
<p>The combination of these four descriptors allow us to override the layout of the fallback font to match the web font, by telling the browser how much space the characters will take up before the web font is downloaded. Read more in the <a href="https://docs.google.com/document/d/1PW-5ML5hOZw7GczOargelPo6_8Zkuk2DXtgfOtJ59Eo/edit">proposal</a>.</p>
<h3 id="implementing-f-mods" tabindex="-1">Implementing f-mods <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#implementing-f-mods" aria-hidden="true">#</a></h3>
<p>The trick to implementing f-mods is to manually define your fallback system fonts with <code>src: local()</code>. This allows us to override the display of the fallback font to match the web font:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@font-face</span></span> <span class="token punctuation">{</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> custom-font<span class="token punctuation">;</span><br /> <span class="token property">src</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"https://example.com/font.woff2"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token atrule"><span class="token rule">@font-face</span></span> <span class="token punctuation">{</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> fallback-font<span class="token punctuation">;</span><br /> <span class="token property">src</span><span class="token punctuation">:</span> <span class="token function">local</span><span class="token punctuation">(</span>Arial<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">/* required! */</span><br /> <span class="token property">advance-override</span><span class="token punctuation">:</span> xx<span class="token punctuation">;</span><br /> <span class="token property">ascent-override</span><span class="token punctuation">:</span> xx<span class="token punctuation">;</span><br /> <span class="token property">descent-override</span><span class="token punctuation">:</span> xx<span class="token punctuation">;</span><br /> <span class="token property">line-gap-override</span><span class="token punctuation">:</span> xx<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token selector">body</span> <span class="token punctuation">{</span><br /> <span class="token property">font-family</span><span class="token punctuation">:</span> custom-font<span class="token punctuation">,</span> fallback-font<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>Calculating the required values might seem complex, but the information we need is already in your web font file. First you'll need the TTF file for your font; if you use Google Fonts then you can check find the TTF in the <a href="https://github.com/google/fonts/tree/master/ofl/">GitHub repository</a>. If you have licensed a font from a foundry then the TTF should have been supplied already.</p>
<p>Once you have the TTF head on over to <a href="https://fontdrop.info/">FontDrop</a> and upload the file. Open the <strong>Data</strong> tab and scroll to the <code>hhea - Horizontal Header Table</code>. There you should find four key values: <code>ascender</code>, <code>descender</code> , <code>line-gap</code> and <code>advanceWidthMax</code>.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/fontdrop.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/GUOLgC7Bq3-578.avif 578w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/GUOLgC7Bq3-578.webp 578w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of font drop showing font HHEA table" loading="lazy" decoding="async" src="https://simonhearne.com/img/GUOLgC7Bq3-578.jpeg" width="578" height="386" /></picture></a><figcaption>HHEA table data in FontDrop</figcaption></figure>
<p>These values are exactly what we need to set the font-face modifiers! In this case the web font has unitsPerEm = 1000 and ascender = 1027, so it's best to set <code>ascent-override: 102.7%</code> in the fallback font face. I recommend experimenting with values until you find the right balance, using the playground below.</p>
<h3 id="f-mods-playground" tabindex="-1">F-mods Playground <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#f-mods-playground" aria-hidden="true">#</a></h3>
<p>Use <a href="https://codepen.io/simonjhearne/pen/rNMGJyr">this codepen</a> to load your custom and fallback fonts, then play with the overrides to get a perfect match! Note that this will be specific to the operating system you are using, it may be worth validating across multiple devices.</p>
<p class="codepen" data-height="852" data-theme-id="light" data-default-tab="result" data-user="simonjhearne" data-slug-hash="rNMGJyr" style="height: 852px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="font-face descriptor playground">
<span>See the Pen <a href="https://codepen.io/simonjhearne/pen/rNMGJyr">
font-face descriptor playground</a> by Simon Hearne (<a href="https://codepen.io/simonjhearne">@simonjhearne</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script defer="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h3 id="f-mods-limitations" tabindex="-1">F-mods limitations <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#f-mods-limitations" aria-hidden="true">#</a></h3>
<p>F-mods only really modify vertical spacing and positioning. This means that character- and letter-spacing still need to be dealt with otherwise you could have words breaking lines at different points, leading to a change in element heights and thus layout shifts. Unfortunately the <code>letter-spacing</code> and <code>word-spacing</code> properties are not available in the @font-face declaration so they must be declared on the body or an element. This means we may still have some work to do in order to prevent layout shift.</p>
<p>If the character and letter spacing needs to be modified for the web font, will need to apply CSS rules to all elements with the web font applied for when the fallback font is shown. This style must then be removed when the web font is loaded. This is possible using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API">font loading API</a>, but the whole process feels racy and fragile. Changing layout-critical CSS with JavaScript doesn't sit right with me.</p>
<p>Browser support for font-face descriptors is... well it's only in Chrome, in v87+. There are, however, positive signals from Safari. No matter the browser support, font-face descriptors are a progressive enhancement so there is no reason not to implement them now.</p>
<h2 id="guaranteeing-fonts-load-in-time" tabindex="-1">Guaranteeing fonts load in time <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#guaranteeing-fonts-load-in-time" aria-hidden="true">#</a></h2>
<p>It has been suggested that fonts can be embedded in CSS as Base64 strings, thus removing the need for additional font requests and ensuring that the font is available at the time of rendering text. I have seen this on production websites, and it does achieve these goals.</p>
<p>Embedding fonts as Base64 strings comes with a number of drawbacks, as such I would not recommend this approach:</p>
<ol>
<li>Font files are compressed binary objects, encoding as Base64 strings will inflate the size significantly. gzip or brotli compression of the CSS bundle will not totally make up for this inflation.</li>
<li>The fonts will be sent to every browser, even if they can't use them (e.g. Opera Mini and IE11 for woff2, users of <a href="https://chrome.google.com/webstore/detail/dyslexia-friendly/miepjgfkkommhllbbjaedffcpkncboeo?hl=en">Dyslexia Friendly</a>)</li>
<li>Fonts rarely change, but CSS changes often - this will reduce cache effectiveness for fonts as every CSS change will invalidate the whole bundle</li>
<li>Inflating CSS size will almost certainly delay page render</li>
<li>Embedding fonts prevents you from effectively using subsets and unicode-range for different alphabets</li>
</ol>
<p>In general, embedding fonts in CSS breaks the magic of web fonts: that browsers know what they need and will download it when they need it. There are two other options if you absolutely require the fonts to be available before the page renders:</p>
<ol>
<li>Hide the body until the font is ready, using <code>opacity: 0</code> and the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API">Font Loading API</a> to show content once the font loads (with a sensible timeout)</li>
<li>Use <code>display:block</code> and match the fallback font as closely as possible to your web font</li>
</ol>
<p>Option 1 would need a lot of testing to ensure that content is always shown eventually, I would avoid it entirely.</p>
<h2 id="the-future-of-font-performance" tabindex="-1">The future of font performance <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#the-future-of-font-performance" aria-hidden="true">#</a></h2>
<p>It is clear that font performance is not yet a solved problem. Thankfully, there a works afoot to improve this in the <a href="https://www.w3.org/Fonts/WG/">W3C Web Fonts Working Group</a>. One of the proposals is progressive font enrichment, this is an incremental font loading concept which allows browsers to request exactly the characters they need to render text, potentially drastically reducing initial font file size and thus improving render speed.</p>
<p>Google <a href="https://fonts.gstatic.com/experimental/incxfer_demo">has a demo</a> of this incremental transfer concept, and <a href="https://twitter.com/jpamental">Jason Pamental</a> has written up the concept in <a href="https://rwt.io/typography-tips/progressive-font-enrichment-reinventing-web-font-performance">much more detail</a> than I could here.</p>
<p>Whilst the incremental font loading concept is interesting, the main use case will be for very large font sets for non-latin languages. There are potential issues of cache dilution to resolve and the overall size of fonts delivered could exceed the optimal subset font file. Watch this space for more developments!</p>
<h2 id="in-conclusion" tabindex="-1">In conclusion <a class="direct-link" href="https://simonhearne.com/2021/layout-shifts-webfonts/#in-conclusion" aria-hidden="true">#</a></h2>
<p>Layout shifts are bad for user experience and tricky to resolve. Rendering text without layout shifts is much more complex than it should be! You can avoid layout shifts entirely if you can apply <code>font-display: optional</code> to your web fonts, otherwise it's a case of racing the browser - trying to get your fonts to the browser before it starts to render text.</p>
<p>Racing the browser is possible by optimising your font files:</p>
<ul>
<li>Use <code>woff2</code> to minimise file size</li>
<li>Serve fonts from your own domain</li>
<li>Preload critical fonts</li>
<li>Subset the font to required characters</li>
<li>Limit the number of weight variations used</li>
<li>Explore variable fonts</li>
<li>Experiment with system fonts</li>
<li>Use f-mods to reduce the impact of font swaps</li>
</ul>
<p>As always, test all changes with real users and only keep them if you see a positive change. On this site I have set the body text to <code>font-display: optional</code> but my heading font and the italic variant of the body font to <code>font-display: swap</code>. Swapping in the heading font and italic variant do not cause a layout shift in most cases (except multi-line headings) so this is a good compromise between design and performance, in my opinion.</p>
Optimistic UI Patterns for Improved Perceived Performance2021-01-15T00:00:00Zhttps://simonhearne.com/2021/optimistic-ui-patterns/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#introduction" aria-hidden="true">#</a></h2>
<p>Web performance is often seen as a technical discipline, arranging bits and bytes to shave off milliseconds of load time, applying best practices, using the latest protocols and formats. Users don't perceive milliseconds though, they don't (typically) care if you use HTTP/1.1 vs HTTP/2 or JPEG vs WebP. Web performance optimisation is really optimising the speed of user experiences, Sergey Chernyshev even <a href="https://simonhearne.com/2020/web-performance-rebrand/#comments">thinks we should</a> rebrand web performance to <strong>UXSpeed</strong>!</p>
<p>If web performance isn't all about technology, it must be about user experience design: orchestrating the web experience to create user delight. There are a number of techniques which aim to improve the speed of UX including <a href="https://cloudinary.com/blog/low_quality_image_placeholders_lqip_explained">low quality image placeholders</a>, <a href="https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a">skeleton UI</a> and optimistic UI patterns.</p>
<p>I love optimistic UI patterns because they can often have a great return on investment: a big impact on user experience for a relatively small engineering effort. These UI patterns assume the best but prepare for the worst, optimising the UI feedback loop to give a better user experience.</p>
<h2 id="feedback-first" tabindex="-1">Feedback First! <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#feedback-first" aria-hidden="true">#</a></h2>
<figure class=" clickable"><a href="https://simonhearne.com/images/optimistic-ui/optimistic-ui.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/OplmOpAhrD-600.avif 600w, https://simonhearne.com/img/OplmOpAhrD-900.avif 900w, https://simonhearne.com/img/OplmOpAhrD-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/OplmOpAhrD-600.webp 600w, https://simonhearne.com/img/OplmOpAhrD-900.webp 900w, https://simonhearne.com/img/OplmOpAhrD-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/OplmOpAhrD-600.jpeg 600w, https://simonhearne.com/img/OplmOpAhrD-900.jpeg 900w, https://simonhearne.com/img/OplmOpAhrD-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="hand drawn image of a close feedback loop to user action" loading="lazy" decoding="async" src="https://simonhearne.com/img/OplmOpAhrD-600.jpeg" width="1200" height="636" /></picture></a><figcaption>Decouple the feedback loop from the execution loop</figcaption></figure>
<p>Optimistic UI patterns have a simple goal: to give the feeling of a robust and instant UI. As humans we start to notice delays around 100ms and frustration increases with the delay, remember the 300ms <a href="https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away">mobile tap delay?</a> A round-trip to a server might take 50ms in the best case scenario, but a large number of requests will exceed 100ms due to network latency and varying server response times. Waiting for a network request means that your UI is bound by variables outside of your control, like your users' network connectivity and device performance.</p>
<blockquote class="success"><p>Optimistic UI patterns decouple user feedback from the network.</p></blockquote>
<p>Optimistic UI patterns decouple user feedback from the network, giving you more control of the speed of the user experience. There are some great opportunities to improve the perceived performance of most pages by applying these patterns to key user interaction points, let's look at some examples!</p>
<h3 id="optimistic-cart-ui" tabindex="-1">Optimistic Cart UI <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#optimistic-cart-ui" aria-hidden="true">#</a></h3>
<p>Allbirds is a New Zealand-American company which designs and sells footwear. The Allbirds website feels very fast to navigate, they use a number of optimistic techniques to achieve this sensation even on poor connections. One of my favourite uses of optimistic UI is the Allbirds mini-cart. Anyone who has worked on a retail site will tell you that cart calls are expensive - they cannot be cached and typically require multiple database queries, this means that adding to cart is often a slow user experience. Allbirds have made efforts to resolve this delay by optimistically sliding in the mini cart <em>immediately</em> after a product is added, even when the API call has not yet hit the server. This does two things: it gives instant feedback to the user that their interaction has been registered, plus the slight animation to slide in the mini-cart buys some active time from the user to make the process feel even faster.</p>
<figure>
<video style="height: 600px;max-height:80vh;width:auto" autoplay="" loop="" muted="" playsinline="" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/h_600,q_auto/v1612776931/videos/allbirds-mobile.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/h_600,q_auto/v1612776931/videos/allbirds-mobile.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/h_600,q_auto/v1612776931/videos/allbirds-mobile.mov" type="video/mov" />
</video>
<figcaption>Allbirds optimistically loads the mini-cart on add to cart</figcaption>
</figure>
<p>This subtle animation only takes half a second, but it buys enough time for the API call to be dispatched to the server and perhaps even for the response to be delivered. This combination of effects culminates in an extremely fast experience, prioritising user feedback in the UI.</p>
<p>This principle can be used any time an action triggers a server-bound UI change, such as creating an account, entering delivery details or submitting information for an insurance quote.</p>
<h3 id="optimistic-atomic-actions" tabindex="-1">Optimistic Atomic Actions <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#optimistic-atomic-actions" aria-hidden="true">#</a></h3>
<p>You may have noticed that Twitter's UI feels remarkably fluid. One of the features which contributes to this experience is the favourite button.</p>
<figure>
<video style="max-width:500px" autoplay="" loop="" muted="" playsinline="" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/twttr_favourite.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/twttr_favourite.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/twttr_favourite.mov" type="video/mov" />
</video>
<figcaption>Twitter's favourite button gives instant feedback</figcaption>
</figure>
<p>Note that the animation starts as soon as you click, giving an immediate feedback response. This happens separately and in parallel to the actual API call to log the action. Further to the instant animation, the API call is resilient to network failures. If the API call fails it is added to a queue to be retried later. Only if the API call fails multiple times is the action "undone" in the UI - the heart icon simply fades back to the unchecked state.</p>
<p>This principle can be applied to any atomic action like starring a product, liking a message or saving an article for later.</p>
<h3 id="contextual-buttons" tabindex="-1">Contextual Buttons <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#contextual-buttons" aria-hidden="true">#</a></h3>
<p>Active states have become less popular as <a href="https://www.interaction-design.org/literature/topics/skeuomorphism">skeuomorphism</a> has lost favour in UI design. Active states give users <em>immediate</em> feedback from their action, like a shadow to indicate a depressed button. You can do more than just add a shadow, though. In this example we give immediate physical feedback, like a real button might behave, then provide further feedback related to the user's action right in their point of focus. The proximity of feedback to the user's focus is key here:</p>
<style>:root{--color-blue:dodgerblue;--color-purple:rebeccapurple;}@-webkit-keyframes short-press{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.9);transform:scale(.9)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes short-press{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(.9);transform:scale(.9)}100%{-webkit-transform:scale(1);transform:scale(1)}}.submit-button{display:block;overflow:hidden;margin:0.5em auto;zoom:2;font-family:inherit;font-weight:300;font-size:.75em;height:20px;line-height:20px;position:relative;background:var(--color-blue);border-radius:3px;box-shadow: 0px 1px 2px 0px rgb(60 54 68);border:0;cursor:pointer;-webkit-transition:all .3s ease;transition:all .3s ease}.submit-button:hover{background:var(--color-purple);outline:0}.submit-button.animated{-webkit-animation:.5s short-press cubic-bezier(.77,0,.175,1) forwards;animation:.5s short-press cubic-bezier(.77,0,.175,1) forwards}.submit-button>span{display:block;color:#fff;text-align:center}.submit-button>span.pre-state-msg{margin-top:-2px;-webkit-transition:all .7s cubic-bezier(.77,0,.175,1);transition:all .7s cubic-bezier(.77,0,.175,1);-webkit-transition-delay:.5s;transition-delay:.5s}.submit-button.state-1 .pre-state-msg{margin-top:-22px}.submit-button.state-2 .pre-state-msg{margin-top:-42px}.submit-button.animated{background:var(--color-purple)}.submit-button span{user-select:none}</style>
<p style="text-align:center">click me 👇<br />
<button class="submit-button state-0"><span class="pre-state-msg">Search</span><span class="current-state-msg">Searching...</span><span class="done-state-msg">Done!</span></button>
</p>
<script>const button=document.querySelector(".submit-button"),stateMsg=document.querySelector(".pre-state-msg"),updateButtonMsg=function(){button.classList.add("state-1","animated"),setTimeout(finalButtonMsg,2e3)},finalButtonMsg=function(){button.classList.add("state-2"),setTimeout(setInitialButtonState,2e3)},setInitialButtonState=function(){button.classList.remove("state-1","state-2","animated")};button.addEventListener("click",updateButtonMsg);</script>
<p>This principle can be applied anywhere a button press results in a server request but doesn't take the user away from the page, like add to cart, search or product configuration buttons.</p>
<h3 id="search-filters" tabindex="-1">Search Filters <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#search-filters" aria-hidden="true">#</a></h3>
<p>Back to Allbirds for this one. Filtering products is often a painful user experience which can impact average order value and conversion rate. Allbirds have added an interstitial state to their filter UI which moves the content down and adds a spinner, making the experience feel more fluid once the results are delivered over the network.</p>
<figure>
<video style="max-width:80%" muted="" playsinline="" autoplay="" loop="" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_auto,q_auto/v1612776931/videos/socks.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_auto,q_auto/v1612776931/videos/socks.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_auto,q_auto/v1612776931/videos/socks.mov" type="video/mov" />
</video>
<figcaption>Allbirds has an interstitial state while waiting for filtered results</figcaption>
</figure>
<p>This interstitial state does not need to serve any functional purpose - it just helps to give the user confidence that the website is working while the browser waits for server response of filtered results. This principle can be applied to any filtering UI which results in a server request.</p>
<h3 id="page-transitions" tabindex="-1">Page Transitions <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#page-transitions" aria-hidden="true">#</a></h3>
<p>Some actions will always add a delay to the user interface, such as navigating to a new page in a traditional web application. This delay between clicking a link and the page context changing can take multiple seconds due to delays outside of your control. The browser's context will not switch to the next page until it can paint something to screen, often meaning multiple assets need to be downloaded first. Even if all blocking CSS & JavaScript is available in cache, the time taken to download and parse the HTML document and then retrieve the cached static assets and render the page can take over a second.</p>
<p>This inter-page delay can be jarring to the user experience, especially if page links do not have good active states. Thoughts of "did I click on that link?" and "is the site broken? I should click again." will start within fractions of a second with no UI feedback. A simple hack to improve the perceived performance in this scenario is to introduce a page transition: a simple spinner or blur effect to indicate that the navigation is occurring.</p>
<figure>
<video style="max-width: 320px" autoplay="" loop="" muted="" playsinline="" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_320,q_auto/v1612776931/videos/jimmychoo-spin.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_320,q_auto/v1612776931/videos/jimmychoo-spin.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_320,q_auto/v1612776931/videos/jimmychoo-spin.mov" type="video/mov" />
</video>
<figcaption>Jimmy Choo animates page transition</figcaption>
</figure>
<p>Jimmy Choo show a simple transition on the unload event of a page which fades away the screen and shows an animated logo. They also have a good active state on the button in this case, making it clear that the user has initiated a navigation. The whole process takes about a second, but feels quite seamless. This principle can be applied anywhere that you have a delayed page transition, especially if you know the target page will be slow such as loading search results or a shopping cart.</p>
<h3 id="pre-emptive-loading" tabindex="-1">Pre-emptive Loading <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#pre-emptive-loading" aria-hidden="true">#</a></h3>
<p>Users constantly give you hints as to what they are going to do next when they hover on and touch elements. By attaching to the <code>mouseover</code> and <code>touchstart</code> events on links we can respond to these hints and use them to buy us some time. On my homepage I inject a <code><link rel='prefetch'></code> into the document if you hover or touch one of the article cards, with the <code>href</code> attribute set to the target blog post. This tells the browser to start fetching the document so that the document should be already downloaded by the time the user's click or tap is complete. This should mean that the page can be served almost instantly from cache!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/optimistic-ui/prefetch-cache.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/pwygS4HDrW-600.avif 600w, https://simonhearne.com/img/pwygS4HDrW-900.avif 900w, https://simonhearne.com/img/pwygS4HDrW-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/pwygS4HDrW-600.webp 600w, https://simonhearne.com/img/pwygS4HDrW-900.webp 900w, https://simonhearne.com/img/pwygS4HDrW-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/pwygS4HDrW-600.jpeg 600w, https://simonhearne.com/img/pwygS4HDrW-900.jpeg 900w, https://simonhearne.com/img/pwygS4HDrW-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="network tab showing html file served from prefetch cache" loading="lazy" decoding="async" src="https://simonhearne.com/img/pwygS4HDrW-600.jpeg" width="1200" height="459" /></picture></a><figcaption>Prefetch cache should be much faster than the network</figcaption></figure>
<p>The open source project <a href="https://instant.page/">instant.page</a> is a generalised application of this principle, although it is trivial to build it yourself in a few lines of JavaScript.</p>
<h2 id="in-conclusion" tabindex="-1">In Conclusion <a class="direct-link" href="https://simonhearne.com/2021/optimistic-ui-patterns/#in-conclusion" aria-hidden="true">#</a></h2>
<p>Web performance is as much art as it is science. Consider how your UI behaves under less-than-ideal conditions and how you can implement optimistic UI patterns to decouple perceived performance from application performance.</p>
<p>The examples we have seen all apply some optimism - either that the request will succeed or that the user will complete an action. Optimism allows us to preempt an outcome and deliver it faster, resulting in happier users. Plus it's nice to raise tickets with <strong>optimistic</strong> in the title!</p>
<p>Sometimes small changes can have a big impact. Focus on changes which improve the feedback loop to user interactions and aim for under 100ms to deliver a UI response. Make a change, measure the impact on user experience and keep it if business outcomes improve!</p>
Alternatives to Spinners on the Web2020-12-22T00:00:00Zhttps://simonhearne.com/2020/alternatives-to-spinners/<p>Animated spinners are one of the lingering legacies of the 1990's web.</p>
<p>In this post I will use an insurance quote service as an example, but these techniques can be applied anywhere that a spinner is currently used. First, though, let's look at the psychology of waiting.</p>
<h2 id="rules-for-waiting-time" tabindex="-1">Rules for waiting time <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#rules-for-waiting-time" aria-hidden="true">#</a></h2>
<p>In <a href="https://davidmaister.com/articles/the-psychology-of-waiting-lines/">The Psychology of Waiting Lines</a>, David H. Maister describes the psychology of queueing. His research is based on real-world queues, but the psychology is applicable any time that people are forced to wait to complete a task - except that people have even less patience on the web, with an unlimited number of websites and applications to distract them from the current task.</p>
<p>I've extracted three key concepts which are most applicable to waits on the web:</p>
<ol>
<li>Unexplained waits feel longer than explained waits</li>
<li>Unoccupied time feel longer than occupied time</li>
<li>Anxiety makes waits feel longer</li>
</ol>
<h3 id="explain-the-wait" tabindex="-1">Explain the wait <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#explain-the-wait" aria-hidden="true">#</a></h3>
<blockquote class="thought"><p>Unexplained waits feel longer than explained waits</p></blockquote>
<p>Spinners don't explain why a user is waiting, just that they have to wait. Giving the user an explanation of why they have to wait can be a simple step to reduce the perceived duration of the wait; a secondary benefit is that the time taken to read any explanation is active time - this can buy us a second or two of wait time!</p>
<p>For our insurance website, we can humanise the wait with an explanation:</p>
<blockquote>
<p>We are generating your quote, evaluating the criteria specific to your situation and determining the best possible price and coverage. This might take up to a minute.</p>
</blockquote>
<p>You may have seen this in action on travel websites or even retail sites. For example Amazon will add a message when calling APIs for stock levels of reseller products and comparison sites will often have text describing how many quotes have been retrieved.</p>
<div class="two-fig-cols">
<figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/amazon-rummaging.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/vu8VhkcEHn-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/vu8VhkcEHn-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of amazon waiting screen which says: 'rummaging around in the stock room'" loading="lazy" decoding="async" src="https://simonhearne.com/img/vu8VhkcEHn-600.jpeg" width="600" height="608" /></picture></a><figcaption>Amazon's humanising wait text</figcaption></figure><figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/amazon-rummaging.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/vu8VhkcEHn-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/vu8VhkcEHn-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of amazon waiting screen which says: 'rummaging around in the stock room'" loading="lazy" decoding="async" src="https://simonhearne.com/img/vu8VhkcEHn-600.jpeg" width="600" height="608" /></picture></a><figcaption>Amazon's humanising wait text</figcaption></figure>
</div>
<h3 id="occupy-time" tabindex="-1">Occupy time <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#occupy-time" aria-hidden="true">#</a></h3>
<blockquote class="thought"><p>Unoccupied time feels longer than occupied time</p></blockquote>
<p>In the real world you might pull out your phone whilst waiting in a queue, or pick up a magazine in a waiting room. Users can easily do this on the web: switching tabs or checking in on a social media application. Our goal is to keep the user active on <strong>our</strong> application, so we should aim to occupy the user's time where they are waiting.</p>
<p>This technique has been used for decades in games: in 1998 Namco was <a href="https://www.eff.org/deeplinks/2015/12/loading-screen-game-patent-finally-expires">granted a patent</a> to add mini-games to the loading screen for large games. This was a reaction to the move from cartridge games to CD-ROM which took much longer to load into memory, leading to frustration.</p>
<p>Whilst users on the web might object to playing games on waiting screens, there are other techniques which might achieve the same goal. For our insurance quote, we could add a simple survey to the waiting screen:</p>
<blockquote>
<p>How easy was this quote process? Answering will not interrupt your quote. 1 - very difficult, 10 - super easy.</p>
</blockquote>
<p>It can be even more simple, such as some text body for the user to read. This can be marketed to the user based on the product or service they are waiting for:</p>
<blockquote>
<p>Our insurance products are backed by 50 years of experience, we insure over 2 million people like you every year.</p>
</blockquote>
<p>There may be other opportunities to keep the user engaged, such as showing interim results like SkyScanner or custom quotes like Slack.</p>
<div class="two-fig-cols">
<figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/skyscanner.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.avif 600w, https://simonhearne.com/img/pp8PNKhyYu-900.avif 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.webp 600w, https://simonhearne.com/img/pp8PNKhyYu-900.webp 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.jpeg 600w, https://simonhearne.com/img/pp8PNKhyYu-900.jpeg 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of skyscanner search results page showing results while still searching" loading="lazy" decoding="async" src="https://simonhearne.com/img/pp8PNKhyYu-600.jpeg" width="1200" height="652" /></picture></a><figcaption>SkyScanner starts showing results straight away</figcaption></figure><figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/skyscanner.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.avif 600w, https://simonhearne.com/img/pp8PNKhyYu-900.avif 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.webp 600w, https://simonhearne.com/img/pp8PNKhyYu-900.webp 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/pp8PNKhyYu-600.jpeg 600w, https://simonhearne.com/img/pp8PNKhyYu-900.jpeg 900w, https://simonhearne.com/img/pp8PNKhyYu-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of skyscanner search results page showing results while still searching" loading="lazy" decoding="async" src="https://simonhearne.com/img/pp8PNKhyYu-600.jpeg" width="1200" height="652" /></picture></a><figcaption>SkyScanner starts showing results straight away</figcaption></figure>
</div>
<h3 id="create-certainty" tabindex="-1">Create certainty <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#create-certainty" aria-hidden="true">#</a></h3>
<blockquote class="thought"><p>Anxiety makes waits feel longer</p></blockquote>
<p>Knowing that you have to wait is bad enough, but not knowing how long you need to wait makes it much more frustrating. Think back to the last time your flight or bus was delayed without an ETA. You cannot make plans without knowing when you will depart or arrive - do you have enough time to use the restrooms or get some lunch? Will you need to change a transfer?</p>
<p>Amusement parks often have estimated wait times in queues, this is done to reduce wait anxiety! It also serves a secondary benefit: the estimated wait times are almost always an over-estimate, so that as you progress through the queue you feel like you are winning back time. If you expected to queue for 45 minutes but got to the end in 30 minutes, you would feel good about the waiting time. Compare that to how it feels to have an unknown wait that ends up taking 30 minutes, like in a queue at a shop or government office.</p>
<p>Doing this on the web can be tricky, but I'm certain that somewhere in your business you have measurements of the duration of complex tasks. For example an insurance company knows that it takes on average 30s to generate a quote, with the 95th percentile task taking 60s (that means that 95% of quotes will be generated within a minute). This data can be used to create messaging on the web:</p>
<blockquote>
<p>Please wait while we generate your quote, this should take thirty seconds</p>
</blockquote>
<p>At the point that 30s has passed, update the message:</p>
<blockquote>
<p>This is taking a little longer than normal but the quote should be generated in just a minute</p>
</blockquote>
<p>Once the 60s mark has gone, it's time for an apologetic message:</p>
<blockquote>
<p>We're sorry this is taking longer than usual, please bear with us as we generate your quote. If you'd like to speak to us on the phone, please call +1 555 1234</p>
</blockquote>
<h2 id="alternatives-to-spinners" tabindex="-1">Alternatives to spinners <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#alternatives-to-spinners" aria-hidden="true">#</a></h2>
<p>Consider the three concepts above: explanation, occupying time and certainty. Spinners don't really solve for any of these! Let's look at three alternatives.</p>
<h3 id="1-show-progress-indication" tabindex="-1">1. Show progress indication <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#1-show-progress-indication" aria-hidden="true">#</a></h3>
<p>One of the frustrations of spinners is that they don't convey any progress, just a perpetual animation that <em>might</em> mean that something is happening in the background.</p>
<p>Users want to know that something is really happening, and that there is progress towards completion. It is tempting to replace a spinner with a progress bar - but this only works if you can show real progress. Fake progress bars might be worse than spinners, especially when they get stuck at 99% and cause user frustration!</p>
<p>It is best to complete longer tasks earlier if you can get stages of feedback to update your progress bar, users want to see feedback early but become less patient as they wait.</p>
<p>You can estimate the speed of the progress bar using historical data if you can't get realtime feedback from the application. In our insurance example we know that 95% of quotes are returned in a minute or less, so we can use that as our extent on the progress bar. A trick to make the progress feel a little faster is to use a non-linear progression: accelerate to make the initial few seconds feel faster, and decelerate at the end to buy a little more time. Research on <a href="https://www.chrisharrison.net/index.php/Research/ProgressBars">non-linear progression</a> shows that users prefer accelerating progress indicators, although this research did not include multi-phase progression (e.g. accelerating and decelerating).</p>
<p>All of these progress indicators have the same total duration, which one feels faster?</p>
<figure>
<div class="progress_bar progress_bar_linear"><span>linear</span></div>
<div class="progress_bar progress_bar_decel"><span>decelerates</span></div>
<div class="progress_bar progress_bar_accel"><span>accelerates</span></div>
<div class="progress_bar progress_bar_both"><span>accelerates and decelerates</span></div>
<figcaption>Various easing methods on progress indicators</figcaption>
</figure>
<h3 id="2-remove-the-spinner" tabindex="-1">2. Remove the spinner <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#2-remove-the-spinner" aria-hidden="true">#</a></h3>
<p>If your spinner is a placeholder for other content - like images or dynamic forms - you might be able to get away with simply removing it!</p>
<p>Spinners appearing and disappearing can be disorienting, so if the wait time is under a second or two it is worth considering removing the spinner. Good alternatives to spinners are placeholder elements such as low quality image placeholders or skeleton UI.</p>
<div class="two-fig-cols">
<figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/simon-sqip.gif"><picture><source type="image/avif" srcset="https://simonhearne.com/img/yMaXRBbSg_-436.avif 436w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/yMaXRBbSg_-436.webp 436w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="simon's logo switching between SVG placeholder and downloaded image" loading="lazy" decoding="async" src="https://simonhearne.com/img/yMaXRBbSg_-436.jpeg" width="436" height="438" /></picture></a><figcaption>SQIP on this site</figcaption></figure>
<figure class=" clickable"><a href="https://simonhearne.com/images/alternatives-to-spinners/skeleton.gif"><picture><source type="image/avif" srcset="https://simonhearne.com/img/lHN1-JwwJd-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/lHN1-JwwJd-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="comparison of a page loading with a spinner and a skeleton UI" loading="lazy" decoding="async" src="https://simonhearne.com/img/lHN1-JwwJd-600.jpeg" width="600" height="308" /></picture></a><figcaption>Simple skeleton UI</figcaption></figure>
</div>
<h3 id="3-keep-a-spinner-but-make-it-nice" tabindex="-1">3. Keep a spinner, but make it nice <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#3-keep-a-spinner-but-make-it-nice" aria-hidden="true">#</a></h3>
<p>If there really is no way to determine how long a wait will be, we might need to keep a spinner. In this case, the spinner should be branded and accompanied by some explanatory text.</p>
<p>Animated gif files are bulky and have a limited colour palette - SVGs and CSS animations are the way forward. These will render nearly instantly (assuming inline SVG and CSS) and will look great on retina displays. Take a look at <a href="http://loading.io/">loading.io</a> for some inspiration, or read <a href="https://simonhearne.com/2020/alternatives-to-spinners/cassie.codes/posts/creating-my-logo-animation/">Cassie Evans' blog post</a> on how they created their animated logo for some advanced techniques!</p>
<p class="codepen" data-height="400" data-theme-id="light" data-default-tab="result" data-user="simonjhearne" data-slug-hash="YzGxmrd" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Cassie!">
<span>See the Pen <a href="https://codepen.io/cassie-codes/pen/mNWxpL">
Cassie!</a> by Cassie Evans (<a href="https://codepen.io/cassie-codes">@cassie-codes</a>)
on <a href="https://codepen.io/">CodePen</a>.</span>
</p>
<script defer="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>There are some good examples of custom loaders out in the wild, such as the <a href="https://tfl.gov.uk/">Transport for London</a> travelling bus and <a href="https://jimmychoo.com/">Jimmy Choo's</a> spinning logo which is shown on page transition.</p>
<div class="two-fig-cols">
<figure>
<video title="TfL custom loading spinner" width="620" autoplay="" loop="" muted="" playsinline="" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_620,q_auto/v1612776931/videos/tfl-anim.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_320,q_auto/v1612776931/videos/tfl-anim.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_620,q_auto/v1612776931/videos/tfl-anim.mov" type="video/mov" />
</video>
<figcaption>Transport for London's custom HTML loader</figcaption>
</figure>
<figure>
<img width="140" height="140" src="https://simonhearne.com/images/alternatives-to-spinners/jc-spinner.gif" style="max-width: 200px;" alt="jimmy choo custom loading spinner" />
<figcaption>Jimmy Choo shows a custom spinner between pages</figcaption>
</figure>
</div>
<h2 id="in-summary" tabindex="-1">In summary <a class="direct-link" href="https://simonhearne.com/2020/alternatives-to-spinners/#in-summary" aria-hidden="true">#</a></h2>
<p>Spinners are the most simple option when you have a long task of indeterminate duration, but they may cause user frustration. You should replace ambiguous spinners with better progress indication and add dynamic explanatory text. If you must use a spinner, make sure it uses more modern technologies like CSS and SVG, that it is branded to your site and that it is relevant to the task at hand.</p>
Web Performance Predictions for 20212020-12-10T00:00:00Zhttps://simonhearne.com/2020/predictions-2021/<p>I <a href="https://simonhearne.com/2019/2020-predictions">wrote an article in 2019</a> where I predicted five top technologies for 2020. Well, 2020 was a strange year! Some things were correct though, for example the growth of JAMStack sites: the <a href="https://almanac.httparchive.org/en/2020/jamstack">Web Almanac shows</a> that JAMStack now powers 154% more mobile pages than in 2019!</p>
<p>As for 2021, there are some key trends which I expect to have a significant impact on the web ecosystem:</p>
<ol>
<li><a href="https://simonhearne.com/2020/predictions-2021/#the-page-experience-update">The Page Experience Update</a></li>
<li><a href="https://simonhearne.com/2020/predictions-2021/#better-behaving-third-parties">Better behaving third-parties</a></li>
<li><a href="https://simonhearne.com/2020/predictions-2021/#logic-moving-to-the-edge">Logic moving to the edge / FaaS</a></li>
<li><a href="https://simonhearne.com/2020/predictions-2021/#browser-monopolies">Browser Monopolies</a></li>
<li><a href="https://simonhearne.com/2020/predictions-2021/#http3">HTTP/3</a></li>
</ol>
<h2 id="the-page-experience-update" tabindex="-1">The Page Experience Update <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#the-page-experience-update" aria-hidden="true">#</a></h2>
<p>Performance will become an even stronger ranking factor in mobile search <a href="https://developers.google.com/search/blog/2020/11/timing-for-page-experience">come May 2021</a>. You might be sick of hearing about it by now, but there are hundreds of thousands of sites which currently fail the <a href="https://web.dev/vitals/">core web vitals</a> assessment. <a href="https://almanac.httparchive.org/en/2020/performance#lcp-by-device">57% of pages have a slow mobile LCP</a> and <a href="https://almanac.httparchive.org/en/2020/performance#cls-by-device">44% of pages have a poor CLS on desktop</a>.</p>
<p>The Page Experience Update is the glue that will bind Product, SEO and engineering teams to deliver great web experiences. There are concerns about the key metrics involved, especially as they can only be collected (at the time of writing) on Chromium-based browsers. There's also a concern about the continuing monopoly of Google in both browser and search, but for now I see this as a positive change. I hope that the 2021 web almanac shows a significant improvement in these key metrics as engineering teams are given more resources to work on performance. You can read more about Core Web Vitals (and how to improve them) in <a href="https://simonhearne.com/2020/core-web-vitals/">this blog post</a>.</p>
<h2 id="better-behaving-third-parties" tabindex="-1">Better Behaving Third-Parties <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#better-behaving-third-parties" aria-hidden="true">#</a></h2>
<p>Third-party content is critical to most websites. The <a href="https://almanac.httparchive.org/en/2020/third-parties">web almanac reports</a> that the median web page has 24 separate third-party requests! Data from the web almanac also shows that advertising has the greatest impact on browser CPU, and that the most popular third-parties range from webfonts and JavaScript CDNs to advertising and analytics. We also know that some particular forms of third-parties have a huge impact on performance: blocking tag managers and multivariate testing frameworks. Don't get me started on <a href="https://andydavies.me/blog/2020/11/16/the-case-against-anti-flicker-snippets/">anti-flicker scripts</a>!</p>
<p>The increased focus on performance created by the Page Experience Update will put serious pressure on bad behaving third-parties, at the same time there are a number of web features which will make it easier for them to reduce their performance impact! Take a look at <a href="https://addyosmani.com/blog/import-on-interaction/">the import on interaction pattern</a> and <a href="https://web.dev/trust-tokens/">Trust Tokens</a> to read about some of these.</p>
<p>New technologies and techniques allow third-party providers to reduce their impact on user experience, unfortunately we will still be dependent on them to make these changes! If you have a third-party provider who currently impacts performance, please reach out to them and ask them to reduce their impact! You likely have a better understanding of how their code impacts experience than they do, and they might appreciate your input.</p>
<h2 id="logic-moving-to-the-edge" tabindex="-1">Logic Moving to the Edge <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#logic-moving-to-the-edge" aria-hidden="true">#</a></h2>
<p>Edge compute has been around for a while. Edge is different from traditional function as a service (FaaS) solutions as logic is executed on a much more distributed set of compute nodes. Hundreds of thousands of servers in the case of Akamai!</p>
<p>This new compute model has a number of limitations: state is minimal (all CDNs offer some kind of key-value storage, but not session storage) and execution time is limited (<a href="https://learn.akamai.com/en-us/webhelp/edgeworkers/edgeworkers-user-guide/GUID-F709406E-2D67-4996-B619-91E90F04EDF2.html">100ms on Akamai EdgeWorkers</a>). The distributed compute paradigm is not going to replace your origin servers, or even your serverless deployments; where it excels though is in stripping small elements of stateless logic away from origin and clients.</p>
<p>A good example of logic best deployed at the edge is geolocation. Sure you can redirect users at origin, or have the device behave differently based on a known location, but edge logic can do this quicker and more robustly. Another example is stateless, quick compute requirements like generating tokens, QR Codes or signatures. For example you might use edge logic to quickly generate a hash of a file for duplication detection, or add watermarks to images.</p>
<p>Migrating logic from client-side code means a more consistent and predictable processing model, taking logic from origin means lower latency and greater distribution. Another great use case is simple A/B testing: a few lines of logic at the edge can replace heavy client-side scripts or load balancer configurations, with the obvious performance benefits this will bring.</p>
<p>Andrew Betts gave a <a href="https://ldnwebperf.org/sessions/design-factors-for-decentralised-edge-applications/">fantastic talk</a> on design factors for edge compute at London Web Performance earlier in 2020.</p>
<h2 id="browser-monopolies" tabindex="-1">Browser Monopolies <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#browser-monopolies" aria-hidden="true">#</a></h2>
<p>Do you remember when Chrome was launched? It doesn't seem so long ago but the web has changed a lot since then. Notably, Chrome and Safari together now account for about 83% of all browser usage, according to <a href="https://gs.statcounter.com/browser-market-share#monthly-200901-202011">Statcounter</a>.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/predictions-2021/statcounter.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/e-6Vjb0fKw-600.avif 600w, https://simonhearne.com/img/e-6Vjb0fKw-900.avif 900w, https://simonhearne.com/img/e-6Vjb0fKw-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/e-6Vjb0fKw-600.webp 600w, https://simonhearne.com/img/e-6Vjb0fKw-900.webp 900w, https://simonhearne.com/img/e-6Vjb0fKw-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/e-6Vjb0fKw-600.jpeg 600w, https://simonhearne.com/img/e-6Vjb0fKw-900.jpeg 900w, https://simonhearne.com/img/e-6Vjb0fKw-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="chart of browser usage since 2009 showing Chrome and Safari taking almost all traffic in 2020" loading="lazy" decoding="async" src="https://simonhearne.com/img/e-6Vjb0fKw-600.jpeg" width="1200" height="675" /></picture></a><figcaption>Browser share 2009 - 2020 (source: <a href="https://gs.statcounter.com/browser-market-share#monthly-200901-202011">statcounter.com</a>)</figcaption></figure>
<p>These browsers split the market share, but have very different support levels for modern features. As developers it makes sense to develop and optimise for the majority of users, this means we tend to develop and test on Chrome, with QA on Safari and (perhaps) a few other key browser families.</p>
<p>As Chrome continues to ship draft features before any other browsers (like QUIC, LayoutInstability API and LargestContentfulPaint API) we may see an increasing performance divide between Chrome and all other browsers. How will this impact the web in 2021? I'll leave that as a question for the reader.</p>
<h2 id="http-3" tabindex="-1">HTTP/3 <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#http-3" aria-hidden="true">#</a></h2>
<p>HTTP/3 has been slowly creeping across the web for the past few years, led by Google implementing gQUIC both in their browser and applications.</p>
<p>HTTP/3 is a fundamental change to networking from HTTP/1.1 & HTTP/2, swapping the underlying TCP protocol for UDP and moving more logic into user-space. This means that the protocol can evolve faster than H/1.1 or H/2 ever could. I don't expect to see a huge change in performance immediately, Cloudflare even saw some <a href="https://blog.cloudflare.com/http-3-vs-http-2/">performance degradation.</a> I highly recommend watching Robin Marx's talk <a href="https://www.youtube.com/watch?v=pq_xk_Pecu4">Fixing HTTP/3 and Preparing for HTTP/3 over QUIC</a> for a great primer on HTTP/3 and QUIC.</p>
<p>You can test the impact for yourself using <a href="https://www.webpagetest.org/">WebPageTest</a> (assuming your server or CDN supports QUIC): run a test in a browser that supports HTTP/3, then run again but disable HTTP/3. For Chromium-based browsers you can pass the command-line flag <code>--disable-quic</code> in the Chromium tab.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/predictions-2021/http3-simonhearne.com.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/HzrPdpFu09-600.avif 600w, https://simonhearne.com/img/HzrPdpFu09-900.avif 900w, https://simonhearne.com/img/HzrPdpFu09-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/HzrPdpFu09-600.webp 600w, https://simonhearne.com/img/HzrPdpFu09-900.webp 900w, https://simonhearne.com/img/HzrPdpFu09-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/HzrPdpFu09-600.jpeg 600w, https://simonhearne.com/img/HzrPdpFu09-900.jpeg 900w, https://simonhearne.com/img/HzrPdpFu09-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot webpagetest result showing 100ms improvement on simonhearne.com when loaded over HTTP 3" loading="lazy" decoding="async" src="https://simonhearne.com/img/HzrPdpFu09-600.jpeg" width="1200" height="438" /></picture></a><figcaption>HTTP/3 results in ~100ms first paint improvement on this site, total performance may be slower.</figcaption></figure>
<p>The key message with HTTP/3 is that it won't necessarily have the immediate benefits that we saw with HTTP/2 over HTTP/1.1, but it gives browser and server developers the opportunity to better implement features like prioritisation and congestion control.</p>
<h2 id="in-summary" tabindex="-1">In Summary... <a class="direct-link" href="https://simonhearne.com/2020/predictions-2021/#in-summary" aria-hidden="true">#</a></h2>
<p>2020 was an interesting year, for many reasons! 2021 will continue to bring more focus on web performance as well as some great new technologies to support us building better web experiences. I hope that the increased focus on web performance will build more momentum behind our little industry, and enable us to invest the required resources to keep speeding up the web.</p>
<p>Performance is still a business problem. Modern protocols and tools are useless if we don't spend the time to measure and improve user experience.</p>
The Impact of Let's Encrypt Changes on Android Users2020-11-09T00:00:00Zhttps://simonhearne.com/2020/lets-encrypt-old-android/<p>Let's Encrypt <a href="https://letsencrypt.org/2020/11/06/own-two-feet.html">recently announced</a> that it is moving to using its own root certificate on January 11, 2021. This will reduce operating costs and complexity for Let's Encrypt, helping their mission to make all websites secure by default.</p>
<blockquote class="info"><p>December 21, 2020: Let's Encrypt have now <a href="https://letsencrypt.org/2020/12/21/extending-android-compatibility.html">announced a solution</a> to resolve this issue and prevent any impact to Android users!</p></blockquote>
<p>The potential footprint for this change is huge: Let's Encrypt <abbr title="Domain Validation">DV</abbr> certificates are used on 225 million websites at the time of writing. You and I, however, are unlikely to be impacted in our daily browsing: the new root certificate that will be used was added to browsers in 2017. This means that if you have updated your device in the past three years it will have the new root certificate installed.</p>
<p>Devices that have not been updated since the root certificate was released will not trust certificates issued against it, so older devices will simply not load websites that use certificates issued after January 11, 2021. This is an issue for a couple of device categories: Android and set top boxes (like your Smart TV). Android devices older than version 7.1.1 do not have the new root certificate, <abbr title="set top boxes">STBs</abbr> are harder to define. Who doesn't update their device in three years, you might think. Unfortunately, Android device updates must be pushed by individual manufacturers (in the majority of cases) and devices that are carrier-locked further require the carrier to push updates. Some manufacturers and carriers may have decided to simply stop supporting older devices. System updates may also be too large for users on limited data plans to even consider downloading.</p>
<h2 id="what-s-the-impact" tabindex="-1">What's the impact <a class="direct-link" href="https://simonhearne.com/2020/lets-encrypt-old-android/#what-s-the-impact" aria-hidden="true">#</a></h2>
<p>Globally, 12% of hits from Android tracked in <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">mPulse</a> were from unsupported versions (i.e. <7.1.1) in data sampled in November 2020. This figure varies dramatically by country, though:</p>
<div class="vega-chart loading desktop-only" id="vis" data-spec="/data/old-android/old-android.json"><a target="_blank" href="https://simonhearne.com/data/old-android/chart.svg"><img src="https://simonhearne.com/data/old-android/chart.svg" /></a></div>
<p style="margin-top: 1em;">Some stand out country statistics from the interactive map above:</p>
<ul>
<li><strong>United States:</strong> 8.2%</li>
<li><strong>United Kingdom:</strong> 14.2%</li>
<li><strong>Brazil:</strong> 16.2%</li>
<li><strong>Russia:</strong> 17.0%</li>
<li><strong>India:</strong> 14.6%</li>
<li><strong>China:</strong> 9.6%</li>
</ul>
<p>The data is not exhaustive, and is only measuring hits that identify themselves as Android on websites instrumented with mPulse. The outcome is startling nonetheless.</p>
<blockquote class="error"><p>In some countries over 25% of Android devices will not trust Let's Encrypt certificates issued in 2021.</p></blockquote>
<h2 id="do-you-use-let-s-encrypt" tabindex="-1">Do you use Let's Encrypt? <a class="direct-link" href="https://simonhearne.com/2020/lets-encrypt-old-android/#do-you-use-let-s-encrypt" aria-hidden="true">#</a></h2>
<p>If you don't know whether your current certificate is from Let's Encrypt, because you use a third-party service such as a CDN or hosting company, you can check in your browser:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/lets-encrypt-old-android/cert-check-LE.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/lFh_lfYMhW-600.avif 600w, https://simonhearne.com/img/lFh_lfYMhW-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/lFh_lfYMhW-600.webp 600w, https://simonhearne.com/img/lFh_lfYMhW-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/lFh_lfYMhW-600.jpeg 600w, https://simonhearne.com/img/lFh_lfYMhW-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of certificate information screen accessed from browser address bar, showing a let's encrypt authority" loading="lazy" decoding="async" src="https://simonhearne.com/img/lFh_lfYMhW-600.jpeg" width="900" height="510" /></picture></a><figcaption>Click the padlock to view certificate information</figcaption></figure>
<h2 id="what-you-can-do-about-it" tabindex="-1">What you can do about it <a class="direct-link" href="https://simonhearne.com/2020/lets-encrypt-old-android/#what-you-can-do-about-it" aria-hidden="true">#</a></h2>
<p>Let's Encrypt provides <a href="https://letsencrypt.org/2020/11/06/own-two-feet.html#if-you-are-a-site-owner">advice to site owners</a>. The gist is that although certificates will start to use the new, less compatible root certificate in January, you can force the old certificate until September. If you use <a href="https://certbot.eff.org/">certbot</a> then you just need to use the flag <code>--preferred-chain "DST Root CA X3"</code>, for example.</p>
<p>Other than delaying the inevitable until September 2021, there is nothing that can be done except for switching certificate providers or encouraging your users to install Firefox for Android, which uses its own, updated set of root certificates.</p>
<p>As for the other device categories, such as <abbr title="set top boxes">STBs</abbr>, I would love to hear what folks are doing to prepare for the coming TLS-pocalypse.</p>
The Performance Cost of EV Certificates2020-11-04T00:00:00Zhttps://simonhearne.com/2020/drop-ev-certs/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/drop-ev-certs/#introduction" aria-hidden="true">#</a></h2>
<p>Extended Validation or EV Certificates were once prized for their impression of increased security. Sites with an EV certificate would be rewarded with a nice green padlock and their company name in the address bar, potentially giving customers an increased sense of security when making purchases or entering sensitive data.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/drop-ev-certs/ev-green-padlock.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/MMSONLBF6w-600.avif 600w, https://simonhearne.com/img/MMSONLBF6w-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/MMSONLBF6w-600.webp 600w, https://simonhearne.com/img/MMSONLBF6w-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/MMSONLBF6w-600.jpeg 600w, https://simonhearne.com/img/MMSONLBF6w-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of address bar showing paypal inc company name" loading="lazy" decoding="async" src="https://simonhearne.com/img/MMSONLBF6w-600.jpeg" width="900" height="263" /></picture></a><figcaption>EV Certificates used to give a benefit in the address bar</figcaption></figure>
<p>If you load a page on a domain with an EV certificate now, you will notice there is no longer a nice green padlock and the company name. This change <a href="https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/ev-to-page-info.md">rolled out</a> in Chrome 77 way back in September 2019. Other browsers have followed suit, now a secure page (using any certificate type) will have a small indicator, normally a padlock, and non-secure pages will be highlighted as <code>Not Secure</code>.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/drop-ev-certs/insecure-warning.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/rf5HgSJcrU-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/rf5HgSJcrU-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of address bar showing not secure for http page" loading="lazy" decoding="async" src="https://simonhearne.com/img/rf5HgSJcrU-600.jpeg" width="600" height="567" /></picture></a><figcaption>Non-secure domains are highlighted to the user</figcaption></figure>
<h2 id="the-rise-of-free-certificates" tabindex="-1">The rise of free certificates <a class="direct-link" href="https://simonhearne.com/2020/drop-ev-certs/#the-rise-of-free-certificates" aria-hidden="true">#</a></h2>
<p>There are three primary types of certificate used to secure web traffic:</p>
<ul>
<li><strong>Domain Validation (DV)</strong> validates that the certificate requestor owns the domain</li>
<li><strong>Organisation Validation (OV)</strong> validates that an organisation owns the domain</li>
<li><strong>Extended Validation (EV)</strong> validates that an organisation owns the domain, with rigorous validation</li>
</ul>
<p>DV certificates just require you to prove that you own a domain: using a DNS record, a redirect or a file on your web server. This process can be completely automated. EV and OV certificates require some manual intervention and further checks at renewal.</p>
<p>The screenshots from Chrome below show the information provided to the user for these different certificate types.</p>
<div class="resp-cols-3">
<figure class="no-shadow">
<img src="https://simonhearne.com/images/drop-ev-certs/dv-cert.png" alt="screenshot of address bar showing not secure for http page" maxwidth="300" loading="lazy" clickable="true" />
<figcaption>DV cert info in Chrome</figcaption>
</figure>
<figure class="no-shadow">
<img src="https://simonhearne.com/images/drop-ev-certs/ov-cert.png" alt="screenshot of address bar showing not secure for http page" maxwidth="300" loading="lazy" clickable="true" />
<figcaption>OV cert info in Chrome</figcaption>
</figure>
<figure class="no-shadow">
<img src="https://simonhearne.com/images/drop-ev-certs/ev-cert.png" alt="screenshot of address bar showing not secure for http page" maxwidth="300" loading="lazy" clickable="true" />
<figcaption>EV cert info in Chrome</figcaption>
</figure>
</div>
<p>Making the web secure by default is an initiative I wholly support. <a href="https://letsencrypt.org/how-it-works/">Let's Encrypt</a> is a non-profit certificate authority which allows any website owner to create a DV certificate for free. This makes it easier than ever to deliver pages over secure connections - preventing man-in-the-middle attacks and allowing the use of HTTP/2 which only works over <abbr title="Transport Layer Security">TLS</abbr>. Let's Encrypt is already integrated into most hosting providers and <abbr title="Content Delivery Networks">CDNs</abbr>.</p>
<p>Extended Validation certificates are expensive; they require a legal entity to prove that they own the domain which requires some human intervention. At the time of writing, this results in an EV certificate costing about $125 USD per year. This is not a huge cost, but is infinitely more than free!</p>
<h2 id="so-what-s-the-problem" tabindex="-1">So what's the problem? <a class="direct-link" href="https://simonhearne.com/2020/drop-ev-certs/#so-what-s-the-problem" aria-hidden="true">#</a></h2>
<p>The performance challenge of EV certificates is that they do not fully support <abbr title="Online Certificate Status Protocol">OCSP</abbr> stapling. The online certificate status protocol is a method which allows clients like web browsers to ensure that certificates are valid, by checking with the certificate authority that it has not been revoked. Stapling allows the web server to perform this validation on the server-side and send the validation with the certificate. Without stapling the client has to do the validation. This can be seen in your web performance waterfalls as one or two (or more!) requests which occur during the TLS negotiation of the connection.</p>
<p>This abridged waterfall shows a <a href="https://www.webpagetest.org/result/201104_DiZ9_b464f5f612708262336735a850328a25/">webpagetest run</a> at 3G speed against <a href="http://paypal.com/">PayPal.com</a>. The OCSP revalidation check takes almost a full second, over half of the TTFB for the response and 42% of the whole response.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/drop-ev-certs/ev-hero.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/xotfSTNNj7-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/xotfSTNNj7-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="waterfall of paypal.com showing almost one second of time lost to OCSP revalidation" loading="lazy" decoding="async" src="https://simonhearne.com/img/xotfSTNNj7-600.jpeg" width="600" height="233" /></picture></a><figcaption>OCSP checks impact <abbr title="Time to First Byte">TTFB</abbr></figcaption></figure>
<p>This effect is more pronounced on high-latency connections. The latency here is not just based on the network connection, but also the distance between the user and the OCSP service. Digicert in this case uses Verizon's CDN which has <a href="https://www.verizondigitalmedia.com/technology/our-network/">165 global points of presence</a> (PoPs). Users in Western Australia, most of Africa, Canada and the former Soviet Union will have worse experiences than those in Europe and the United States.</p>
<p>As pointed out by <a href="https://twitter.com/rposbo">Robin Osborne</a> - there are <a href="https://twitter.com/rposbo/status/1462795069996548110?s=20">no OCSP resolvers in China</a>. This means that visitors could have up to a one minute delay before they load your page for the first time, and due to the fact that OCSP status is stored for seven days - this delay will occur once per week for each user!</p>
<p>A European client of mine recently switched from EV to OV and saw a significant performance improvement in TLS negotiation and thus Time to First Byte. The median negotiation time improved by 20% and it halved at the 95th percentile. Note the drop across all percentiles in this <a href="https://www.akamai.com/products/mpulse-real-user-monitoring">mPulse</a> chart!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/drop-ev-certs/tlsperf.jpeg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/IUzpA-ezW2-600.avif 600w, https://simonhearne.com/img/IUzpA-ezW2-900.avif 900w, https://simonhearne.com/img/IUzpA-ezW2-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/IUzpA-ezW2-600.webp 600w, https://simonhearne.com/img/IUzpA-ezW2-900.webp 900w, https://simonhearne.com/img/IUzpA-ezW2-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/IUzpA-ezW2-600.jpeg 600w, https://simonhearne.com/img/IUzpA-ezW2-900.jpeg 900w, https://simonhearne.com/img/IUzpA-ezW2-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="graph showing TLS performance improve across all percentiles after switching from EV to OV certificate" loading="lazy" decoding="async" src="https://simonhearne.com/img/IUzpA-ezW2-600.jpeg" width="1200" height="628" /></picture></a><figcaption>mPulse chart showing TLS negotiation improves after migration from EV to OV certificate.</figcaption></figure>
<blockquote class="success"><p>It is likely that moving from an EV certificate to organisation validation (OV) certificate will reduce costs and improve performance.</p></blockquote>
<p>Note that OCSP stapling must be enabled on your TLS terminating endpoint (e.g. your firewall or CDN) for it to work on DV and OV certificates. You can check your current certificate using the <a href="https://www.ssllabs.com/ssltest/">Qualys SSL Server Test</a>.</p>
<p>For simplicity I have ignored some of the intricacies of how OCSP works with intermediary certificates, and some of the many edge cases that occur with OCSP stapling. <a href="https://twitter.com/TheRealNooshu">Matt Hobbs</a> goes into much more detail in <a href="https://nooshu.github.io/blog/2020/01/26/the-impact-of-ssl-certificate-revocation-on-web-performance/">his blog post on certificate revocation</a> if you crave more technical detail!</p>
Getting Fast and Staying There2020-10-27T00:00:00Zhttps://simonhearne.com/2020/getting-fast-staying-there/<p>If you've ever trained for a big sporting event: a marathon, bike race or similar, you'll know how tough it is to train to get to your peak performance. You'll also know that after the event you feel totally drained, your performance will not be the same for weeks while your body recovers. Training for a tough physical event is a project, a focused effort on getting to a goal. This approach does not work for ultra-marathoners or during the Tour de France, though: their performance must be maintained for days, weeks or months. These elite athletes treat their performance as a feature, something they devote everything to, rather than a project to be executed.</p>
<hr />
<p>Site speed is seen as a technical problem: bits and bytes, microseconds, three-letter-acronyms. Solving site speed is seen as business success, though: faster experiences lead to happier users and better business outcomes.</p>
<p>Site speed is not clear-cut: we know that faster is better, but how much faster and how much better? This varies between sites and there can not be a single answer. If we look at security & accessibility - there are (conceptually) simple end-goals: AA accessibility rating or a successful penetration test. Feature development is similarly simple: did the feature meet the requirements, and did it improve business metrics?</p>
<blockquote class="thought"><p>Site speed is an art: orchestrating technology to deliver a fluid and choreographed user experience.</p></blockquote>
<p>The only way to truly succeed with a site speed project is to make speed a feature: projects end but features survive as long as they deliver value. Features are shared top-down from product to engineering, not the other way around. Features get investment, projects get budgets.</p>
<p>A common challenge when making performance a feature is determining the value that it will bring. Looking at the <a href="https://simonhearne.com/2020/value-of-site-speed/">relationship</a> between performance and user outcomes helps to build the business case, but it doesn't tell the whole story. Take for example the <a href="https://blog.chriszacharias.com/page-weight-matters">YouTube Feather project</a>: a super lightweight version of the YouTube web app. This project reduced the page weight of the video page from 1.2MB to under 100kB and dropped the number of requests by a factor of ten, resulting in a much faster video experience. The real user data, however, showed that page load time <strong>increased</strong> on the lighter, faster pages. The reason? Users who were previously unable to use the web app due to multi-minute load times could now watch videos! The distribution of traffic from low-end devices and countries with poor connectivity increased, pulling the average load times up.</p>
<blockquote><p>...entire populations of people simply could not use YouTube because it took too long to see anything. Under Feather, despite it taking over two minutes to get to the first frame of video, watching a video actually became a real possibility.</p><aside class="attribution"><a href="https://blog.chriszacharias.com/page-weight-matters#os1i4hGykJcr7VJLS1K6fa:~:text=entire%20populations%20of%20people%20simply%20could,video%20actually%20became%20a%20real%20possibility">source</a></aside></blockquote>
<p>As <a href="https://twitter.com/anna_debenham">Anna Debenham</a> says: performance is a basic multiplier. Making your applications faster means they are more accessible to users in the long tail of the performance distribution. A performance feature may bring you significant increases in traffic from new users, those that had previously given up on your application due to bad prior experiences. This is an <a href="https://en.wikipedia.org/wiki/There_are_known_knowns#:~:text=Unknown%20unknowns%20(unexpected%20or%20unforeseeable,recognized%20but%20poorly%20understood%20phenomena.">unknown unknown</a>: you don't know how many users don't use your application!</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">Performance is a basic multiplier. Want more people to use a feature? Make it faster. <br /><br />I was trying to figure out why usage of an old feature had suddenly spiked – we hadn't changed anything about it. 🤔 But we made the page it was on load 4x faster. Duh. <a href="https://t.co/ZZnQh4pdwT">https://t.co/ZZnQh4pdwT</a></p>— Anna Debenham (@anna_debenham) <a href="https://twitter.com/anna_debenham/status/1184543959957291008?ref_src=twsrc%5Etfw">October 16, 2019</a></blockquote>
<p>Site speed is technically a solved problem: ship small files, fast. Unfortunately this goes against the trend for JavaScript frameworks, rich media content, rapid feature development and of course the many third-party services which promise to offer increased revenue or insight.</p>
<blockquote class="success"><p>Making speed a feature reduces <a href="https://en.wikipedia.org/wiki/Social_loafing">social loafing</a> by bringing focus to a common goal.</p></blockquote>
<p>Front-end engineers in the most part know how to deliver a fast experience. QA knows when a release feels slower. Product knows that faster speeds result in better user outcomes (see my <a href="https://simonhearne.com/2020/value-of-site-speed/">blog post on that here</a>). In fact, <strong>anyone</strong> who has anything to do with the website should care about speed, and thus has some responsibility for it. This often creates a <a href="https://en.wikipedia.org/wiki/Diffusion_of_responsibility#Division_of_labor">diffusion of responsibility</a>: when everyone shares responsibility, no-one takes responsibility.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/not-my-problem.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/aLXfzbQh3s-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/aLXfzbQh3s-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/aLXfzbQh3s-600.jpeg" width="600" height="680" /></picture></a><figcaption></figcaption></figure>
<blockquote class="success"><p>Successful speed goals are simple. My favourite: <strong>don't get slower</strong>.</p></blockquote>
<p>Simple goals are the easiest to achieve. Setting a goal as simple as "don't get slower" may seem trivial, but it raises many questions: How fast are we? How do we know? How do we know when it changes? These goals should be shared across the business. In my experience successful site speed initiatives share the following common traits:</p>
<ul>
<li>Speed is understood and supported from the top (CTO has a speed dashboard)</li>
<li>Speed data is socialised and celebrated (live dashboards for all teams, performance budgets)</li>
<li>Engineering is empowered to focus on speed (tooling, speed week, speed as a feature)</li>
<li>Speed is measured in CI & QA (CLI tooling)</li>
<li>Product design accounts for speed (prioritising key visual elements, designing for data-saver mode)</li>
<li>Speed is measured in-live, for real users on real devices (real user monitoring)</li>
</ul>
<p>These can only be achieved when the whole business understands the value of speed, invests in it as a feature and celebrates it as a key to success. There's no point monitoring performance if no-one does anything with the data.</p>
<p>One way to affect positive change is through gamification. Folks love gold stars 🌟: creating performance budgets and goals can help drive an initial performance project to a successful outcome. Be careful with performance budgets though, they can encourage complacency once met and often focus on a small subset of static performance measures. I suggest finding the performance metrics which <a href="https://simonhearne.com/2020/perf-metrics/">best correlate</a> with user outcomes on your application and create a goal based on that, such as <code>75% of our mobile users achieve a largest contentful paint of less than 2.5 seconds on landing pages</code>. This metric also happily matches <a href="https://simonhearne.com/2020/core-web-vitals">Google's goals</a> for optimum SEO benefits.</p>
<blockquote class="warning"><p>There's no point monitoring performance if no-one does anything with the data</p></blockquote>
<p>Investing in site speed might be a leap of faith, but it is one which will always pay off. Start by <a href="https://simonhearne.com/2020/value-of-site-speed/">measuring what matters</a>, then start tracking improvement.</p>
What is the best page speed metric?2020-10-26T00:00:00Zhttps://simonhearne.com/2020/perf-metrics/<p>The recent updates from Google on <a href="https://web.dev/vitals/">Core Web Vitals</a> are a great effort to find universal speed metrics that reflect quality of user experience. Google's goals are closely aligned with yours: fast user experiences result in better business outcomes. Where your goals may differ from Google is that you know your users and your pages better. You know that marketing landing pages have to deliver the right content in the right position, that your product images are more important than social media icons and that your checkout flow must be lightning fast to maximise conversion rate.</p>
<p>How, then, do you determine what metrics to track on your site, and how to set performance goals that reflect optimum user outcomes? Real user monitoring gives us a unique insight here: tracking performance and business metrics in the same place gives us the ability to correlate them.</p>
<h2 id="what-s-out-there" tabindex="-1">What's out there? <a class="direct-link" href="https://simonhearne.com/2020/perf-metrics/#what-s-out-there" aria-hidden="true">#</a></h2>
<p>There are hundreds of performance metrics that we can choose from. The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API">Navigation Timing API</a> defines a bunch, plus there are the Paint Timing API and Chrome-specific metrics like Largest Contentful Paint, synthetic measurements like <a href="https://web.dev/speed-index/">Speed Index</a> and you can also define your own using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API">User Timing API</a>.</p>
<p>Google has put emphasis on two key page speed metrics lately: First Contentful Paint and Largest Contentful Paint. Unfortunately, these metrics are available only on a subset of browsers: FCP is in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming">Paint Timing API</a> and LCP is in its very own <a href="https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint">Largest Contentful Paint API</a>.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/paint-timing-support.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/q2iJiurR4J-600.avif 600w, https://simonhearne.com/img/q2iJiurR4J-900.avif 900w, https://simonhearne.com/img/q2iJiurR4J-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/q2iJiurR4J-600.webp 600w, https://simonhearne.com/img/q2iJiurR4J-900.webp 900w, https://simonhearne.com/img/q2iJiurR4J-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/q2iJiurR4J-600.jpeg 600w, https://simonhearne.com/img/q2iJiurR4J-900.jpeg 900w, https://simonhearne.com/img/q2iJiurR4J-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of mozilla developer network showing poor support for paint timing apis in non-chrome browsers" loading="lazy" decoding="async" src="https://simonhearne.com/img/q2iJiurR4J-600.jpeg" width="1200" height="390" /></picture></a><figcaption>Paint timings are currently only supported in Chromium browsers (<a href="https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint">source on MDN</a>)</figcaption></figure>
<p>So whilst FCP and LCP are undoubtedly good performance metrics, relying on them might exclude a lot of devices from your benchmarks; most notably: iOS Safari.</p>
<h2 id="how-to-determine-what-metrics-work-for-you" tabindex="-1">How to determine what metrics work for you <a class="direct-link" href="https://simonhearne.com/2020/perf-metrics/#how-to-determine-what-metrics-work-for-you" aria-hidden="true">#</a></h2>
<p>Every site is different, so some analysis is required to determine which metrics matter the most to your business outcomes. For one website I have correlated six key performance metrics with four key business outcomes. The performance data in this case is collected from the first page in the session, another analysis would need to be performed on mid-session page views.</p>
<p>Here you can play around with different combinations of performance and business metrics. Note that the three device type categories have very different results!</p>
<div class="vega-chart loading" id="vis1" data-spec="/data/perf-metrics/perf-metrics.json">Loading chart...</div>
<p>These charts all show that performance impacts user outcomes, but there are a few stand-out charts worth highlighting:</p>
<script>
function switchChart(metric,timer) {
if (window.charts.length > 0) {
let view = window.charts[0].view;
let state = view.getState();
state.signals['metric'] = metric;
state.signals['timer'] = timer;
view.setState(state);
}
}
</script>
<ol>
<li>FCP correlates strongly with bounce rate (p=0.8) <button onclick="switchChart('bounce_rate','firstContentfulPaint')">Show chart</button></li>
<li>Mobile & Tablet LCP correlates strongly with conversion rate (p=-0.7) <button onclick="switchChart('conversion_rate','largestContentfulPaint')">Show chart</button></li>
<li>Page Load Time correlates strongly with session length (p=-0.9) <button onclick="switchChart('session_length','timerstload')">Show chart</button></li>
</ol>
<p>I encourage you to think about which user outcomes are most important to your website and build this analysis yourself. Determine the top three performance metrics which correlate most strongly with these outcomes then use this data to set your performance objectives. For example in this data we can deduce that the optimal landing page First Paint for mobile devices (optimising for bounce rate) is 1,200ms.</p>
<p>Landing page LCP has shown the strongest correlation to business outcomes (e.g. conversion) across multiple websites I have analysed, and FCP correlates best with session metrics like bounce rate and session length. Where available, these new paint timings give us the best proxy of user experience. In practice, first paint is more widely supported and is often very close to FCP. LCP can not be polyfilled, but a <a href="https://www.html5rocks.com/en/tutorials/webperformance/usertiming/">user timing mark</a> can be devised for each page template to measure the loading of hero elements, this should provide a good proxy and can be tested against LCP in browsers that support it.</p>
<p>The key takeaway here is to correlate speed metrics with business outcomes, determine which are the best proxies for user experience on <em>your</em> pages and use those to set your speed goals. Read my post on <a href="https://simonhearne.com/2020/getting-fast-staying-there/">getting fast and staying there</a> for more detail on setting goals.</p>
Four Charts That Prove the Value of Site Speed2020-10-22T00:00:00Zhttps://simonhearne.com/2020/value-of-site-speed/<p>Site speed is an important but often-overlooked component in user experience. We know intuitively that slow experiences are unpleasant and make us more likely to leave a site, but building evidence of this effect has always been tricky.</p>
<p>The charts below were built with data collected using mPulse RUM.</p>
<div class="chart-sticky-container" id="sticky-container">
<div class="vega-chart loading chart-animate chart-sticky chart-active" id="vis" data-spec="/data/speed-value/speed-value.json">Loading chart...</div>
<div class="stage" data-stage="0">
<h2 id="slow-landing-high-bounce" class="no-link">Slow Landing Pages = High Bounce Rate</h2>
<p>This chart shows the page speed distribution for an website by device type. The area is the distribution of sessions by average page speed and the lines in this case show the average bounce rate for sessions by the average page speed in the session.</p>
<p>The majority of traffic is mobile, hence the higher blue distribution. This website is well optimised for mobile browsers resulting in a faster average page speed than on tablet or desktop experiences. The distributions show a peak of session average page speed at around 1.2 seconds for mobile and desktop, with tablet lagging slightly at around 1.5s. Note that the page speed here is the average of all page views in a session: including the landing page, browsing and the checkout flow where appropriate.</p>
<blockquote class="error"><p>Users are more likely to leave the site immediately when the first page of a session is slow.</p></blockquote>
<p>The bounce rate lines here show that there is a strong correlation between page speed and bounce rate. The initial drop is due to error pages and bots which result in high bounce rates and very fast pages. There is a settling around the peak of the distribution where we find the optimal bounce rates of 22% on mobile, 14% on desktop and 9% on tablet, these occur at the fastest page speeds of around one second on average!</p>
<p>Users are more likely to leave the site immediately as their page speed gets worse. The mobile bounce rate increases from 22% to 40% as page speed increases from 1s to 2s! The median bounce rate for mobile is 41% at 2.7s, only 50% of sessions achieve this speed.</p>
<blockquote class="warning"><p>Your slowest users will not even appear in your data - they will have left before the page loads <span style="font-style:normal">😱</span></p></blockquote>
</div>
<div class="stage" data-stage="1">
<h2 id="slow-landing-low-conversion" class="no-link">Slow Pages = Low Conversion Rate</h2>
<p>We see a similar pattern when looking at conversion rate. Conversion peaks where page speed is under a second, with conversion rate halving across all device types from experiences of one second to two seconds.</p>
<blockquote class="error"><p>Users are less likely to convert if their average experience during a session is slow.</p></blockquote>
</div>
<div class="stage" data-stage="2">
<h2 id="fast-pages-higher-revenue" class="no-link">Fast Pages = Higher Revenue</h2>
<p>It's not just conversion that takes a hit when users have a slow experience. Average order value drops significantly in the first second here from $190 on desktop at 800ms to $140 at 3s. Faster experiences lead to bigger basket sizes!</p>
<blockquote class="success"><p>Visitors spend more money when the average experience is faster.</p></blockquote>
</div>
<div class="stage" data-stage="3">
<h2 id="fast-pages-longer-sessions" class="no-link">Fast Pages = More Pageviews</h2>
<p>If you make money through advertising, session length is a key performance indicator. In this graph we see that session length correlates with page speed too!</p>
<blockquote class="success"><p>Users visit more pages when the average experience is faster.</p></blockquote>
<p>Here the average session length peaks on desktop at 20 pages per session at a one second average page speed. This drops dramatically to just 13 pages when users have an average 2.25s page speed.</p>
</div>
</div>
<blockquote class="info small"><p>The charts presented here are generated from real data, the explicit values are examplar.</p></blockquote>
<h2 id="conclusions" tabindex="-1">Conclusions <a class="direct-link" href="https://simonhearne.com/2020/value-of-site-speed/#conclusions" aria-hidden="true">#</a></h2>
<p>Page speed is a critical factor in user experience, but most visitors are not even aware of it! Slower experiences are <a href="https://calendar.perfplanet.com/2013/slow-pages-damage-perception/">reported to</a> feel 'basic', 'confusing' and 'clunky', rather than slow. Delivering fast user experiences gives your website or application the best chance at meeting your business objectives and creating positive user experiences.</p>
<script>
window.addEventListener('load',() => {
const sticky_els = document.querySelectorAll("#sticky-container div[data-stage");
const stages = [
"bounce",
"conversion",
"revenue",
"sessionlength"
];
const chartID = "vis";
const signalName = "stage";
var currentStage = sticky_els[0].dataset.stage;
var chartId = -1;
var lastBottom = Infinity;
sticky_els[0].classList.add('stage-active');
const options = {
rootMargin: '-150px',
threshold: 0
};
const updateChart = stage => {
chartElem = document.getElementById(chartID);
chartElem.classList.add('active');
setTimeout(function(){document.getElementById('vis').classList.remove('active')},250)
let c = window.charts.filter(c => c.id === chartID);
if (c.length > 0) {
let chart = c[0].view;
let state = chart.getState();
state.signals[signalName] = stages[stage];
chart.setState(state);
}
};
const toggleActive = stage => {
sticky_els.forEach(e => {
if (e.dataset.stage == stage) {
e.classList.add('stage-active');
} else {
e.classList.remove('stage-active');
}
})
};
const intersector = (entries,observer) => {
entries = entries.filter(e => e.isIntersecting);
if (entries.length >= 1) {
entries = entries.sort((a,b)=>a.intersectionRatio < b.intersectionRatio);
let entry = entries[0];
if (entry.intersectionRect.top > 0) {
let stage = entry.target.dataset.stage;
if (stage && stage !== currentStage) {
currentStage = stage;
updateChart(stage);
toggleActive(stage);
}
}
}
};
tries = 0;
obsInterval = setInterval(() => {
if (window.charts.length > 0) {
console.info("Charts ready, creating observer.");
const observer = new IntersectionObserver(intersector,options);
sticky_els.forEach(el => {
observer.observe(el);
});
clearInterval(obsInterval);
} else {
tries++;
if (tries > 20) {
console.info("Charts not ready after 10 seconds, giving up 😧");
clearInterval(obsInterval);
} else {
console.info("Charts not ready yet, delaying 500ms.");
}
}
},500);
});
</script>
How to Improve Core Web Vitals2020-10-19T00:00:00Zhttps://simonhearne.com/2020/core-web-vitals/<blockquote class="warning"><p>10 Nov update: Google <a href="https://webmasters.googleblog.com/2020/11/timing-for-page-experience.html">has announced</a> that the Page Experience update will go live in May 2021!</p></blockquote>
<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#introduction" aria-hidden="true">#</a></h2>
<p>Google <a href="https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html">announced</a> updates to the Page Experience ranking algorithm in May of 2020. This update includes two key highlights:</p>
<ol>
<li>Three new performance metrics will be used as ranking signals for SEO</li>
<li>Top Stories no longer requires an AMPHTML page, and performance will become a ranking factor</li>
</ol>
<p>The update is great news for site speed fans: Google is putting an even greater emphasis on the speed of user experiences, offering two SEO benefits to site owners who deliver the fast experiences that customers expect.</p>
<blockquote class="info"><p>The Page Experience Update was delayed due to COVID-19. Changes to the ranking algorithm will happen in May 2021</p></blockquote>
<p>To get a feel for your current performance against these metrics, try running a page through <a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a> or <a href="https://web.dev/measure">web.dev/measure</a>. These Google tools will give you a score for performance and let you know how a page measures up against the new performance goals.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/psi.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/xo6mvPjNWy-600.avif 600w, https://simonhearne.com/img/xo6mvPjNWy-900.avif 900w, https://simonhearne.com/img/xo6mvPjNWy-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/xo6mvPjNWy-600.webp 600w, https://simonhearne.com/img/xo6mvPjNWy-900.webp 900w, https://simonhearne.com/img/xo6mvPjNWy-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/xo6mvPjNWy-600.jpeg 600w, https://simonhearne.com/img/xo6mvPjNWy-900.jpeg 900w, https://simonhearne.com/img/xo6mvPjNWy-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/xo6mvPjNWy-600.jpeg" width="1200" height="486" /></picture></a><figcaption>PageSpeed Insights assesses a URL against the Core Web Vitals</figcaption></figure>
<p>These performance metrics have been designed to be universal measures of page speed and to be good proxies for perceived user experience, they should be valid measures of how the experience feels for almost all web pages. That said, Google has stated that these metrics are being reviewed constantly and may change in the future.</p>
<div class="resp-cols-3">
<img src="https://simonhearne.com/images/core-web-vitals/lcp_ux.svg" width="171" height="150" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/fid_ux.svg" width="171" height="150" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/cls_ux.svg" width="171" height="150" loading="lazy" />
</div>
<p><strong>Largest Contentful Paint</strong> is a measure of rendering performance, a good replacement for page load time or DOM content ready.</p>
<p><strong>First Input Delay</strong> is a measure of how long it takes the browser to respond to discrete user input like clicks or taps.</p>
<p><strong>Cumulative Layout Shift</strong> is a measure of how unstable the page was, a sum of all unexpected layout shifts in the page lifecycle.</p>
<p>The recommended values above are determined from the 75th percentile from real user experiences: at least 75% of your page views should exceed the <strong>Good</strong> value to pass this assessment!</p>
<blockquote class="success"><p>Improving your core web vitals will potentially have a positive impact on SEO ranking, it will definitely have a positive impact on user experience!</p></blockquote>
<h2 id="how-google-measures-speed" tabindex="-1">How Google Measures Speed <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#how-google-measures-speed" aria-hidden="true">#</a></h2>
<p>The three new performance metrics are relatively new to the world of web performance, as such they are currently only supported via API in Blink-based browsers (Chrome, Chrome on Android, Chromium Edge). The data that Google will use for the Page Experience update is taken from <a href="https://developers.google.com/web/tools/chrome-user-experience-report">Chrome UX Report (CrUX)</a>, a collection of anonymised performance statistics taken from real page loads in Chrome browsers around the world. CrUX measures all regular page loads, for both landing and mid-session pages, regardless of cache state. It does not measure soft navigations (route changes) within single page applications.</p>
<p>This means that soft navigations will potentially be penalised, with higher than expected CLS scores and missing or unfairly high LCP values. Unfortunately there is not yet a good solution to this problem. This makes it even more important to reduce unexpected layout shifts and optimise LCP as much as possible!</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">So SPA landing pages will be penalised as CLS is summed throughout the whole session (and attributed to landing URL).<br />SPA soft navs will be further penalised as LCP is only calculated on hard navigations (so no data or slow data, depending on app).<br /><br />Unless I'm misunderstanding?</p>— Simon Hearne (@simonhearne) <a href="https://twitter.com/simonhearne/status/1316775506222682112?ref_src=twsrc%5Etfw">October 15, 2020</a></blockquote>
<p>The update comes with a set of recommended speed goals, these are measured at the 75th percentile in the CrUX data so at least 75% of the measured experiences should meet or exceed these goals. The cache and session status are not available in the CrUX dataset so the 75th percentile value is taken from all page loads, cached or not.</p>
<p>PageSpeed Insights will give you three sets of performance metrics, where available:</p>
<ul>
<li><strong>Field</strong> - the data from CrUX for the given URL</li>
<li><strong>Origin summary</strong> - the aggregate field data of all of your pages</li>
<li><strong>Lab</strong> - a test run from a Google server which applies throttling</li>
</ul>
<p>If your page does not have enough traffic to produce the <strong>Field</strong> data you will only get the origin summary result. Recommendations and diagnostics in PageSpeed Insights are produced from the <strong>Lab</strong> test, performance metrics should be taken from the field results. The recommendations should give you a general idea of areas for improvement which will positively impact your web vital results.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/pagespeed.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/mZhzTGf3kR-600.avif 600w, https://simonhearne.com/img/mZhzTGf3kR-900.avif 900w, https://simonhearne.com/img/mZhzTGf3kR-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/mZhzTGf3kR-600.webp 600w, https://simonhearne.com/img/mZhzTGf3kR-900.webp 900w, https://simonhearne.com/img/mZhzTGf3kR-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/mZhzTGf3kR-600.jpeg 600w, https://simonhearne.com/img/mZhzTGf3kR-900.jpeg 900w, https://simonhearne.com/img/mZhzTGf3kR-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/mZhzTGf3kR-600.jpeg" width="1200" height="698" /></picture></a><figcaption>PageSpeed Insights provides recommendations based on static analysis on lab tests</figcaption></figure>
<h2 id="optimising-for-core-web-vitals" tabindex="-1">Optimising for Core Web Vitals <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#optimising-for-core-web-vitals" aria-hidden="true">#</a></h2>
<p>Let's talk about how to measure, diagnose and improve each of the three new metrics: LCP, FID and CLS, in turn. Remember that any optimisations to improve these metrics will almost certainly make your pages feel faster and improve user experience!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/cwv_hero.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/O67se7xkuF-600.avif 600w, https://simonhearne.com/img/O67se7xkuF-900.avif 900w, https://simonhearne.com/img/O67se7xkuF-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/O67se7xkuF-600.webp 600w, https://simonhearne.com/img/O67se7xkuF-900.webp 900w, https://simonhearne.com/img/O67se7xkuF-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/O67se7xkuF-600.jpeg 600w, https://simonhearne.com/img/O67se7xkuF-900.jpeg 900w, https://simonhearne.com/img/O67se7xkuF-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/O67se7xkuF-600.jpeg" width="1200" height="633" /></picture></a><figcaption>How (core) web vitals fit into the page lifecycle</figcaption></figure>
<h3 id="largest-contentful-paint-lcp" tabindex="-1">Largest Contentful Paint (LCP) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#largest-contentful-paint-lcp" aria-hidden="true">#</a></h3>
<p>LCP is a timing point measured when the largest above-the-fold element is painted to the screen. In most cases this will be either a hero image / video, or the main block of text on the page.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/wpt1.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/1h4jxTlUl--600.avif 600w, https://simonhearne.com/img/1h4jxTlUl--900.avif 900w, https://simonhearne.com/img/1h4jxTlUl--1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/1h4jxTlUl--600.webp 600w, https://simonhearne.com/img/1h4jxTlUl--900.webp 900w, https://simonhearne.com/img/1h4jxTlUl--1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/1h4jxTlUl--600.jpeg 600w, https://simonhearne.com/img/1h4jxTlUl--900.jpeg 900w, https://simonhearne.com/img/1h4jxTlUl--1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/1h4jxTlUl--600.jpeg" width="1200" height="340" /></picture></a><figcaption><a href="https://www.webpagetest.org/video/compare.php?tests=201018_DiK0_062c01875d889fe3168bbf1dec82a676-r%3A1-c%3A0&thumbSize=200&ival=100&end=visual&bg=fff&text=333">WebPageTest</a> highlights LCP with a red dashed border on filmstrip frames</figcaption></figure>
<p>LCP is a good alternative to "Page Load Time": it measures everything required for your site to render the key content on a page - DNS, TLS, HTML, CSS and blocking JavaScript, but does not include asynchronous scripts or lazy-loaded content.</p>
<p>LCP occurs only after three stages of page load have completed:</p>
<ol>
<li>HTML delivered and parsed</li>
<li>Critical path assets downloaded, parsed and processed</li>
<li>LCP asset downloaded and rendered (image, video, web font etc.)</li>
</ol>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/wpt2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/yZIs-oFPwg-600.avif 600w, https://simonhearne.com/img/yZIs-oFPwg-900.avif 900w, https://simonhearne.com/img/yZIs-oFPwg-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/yZIs-oFPwg-600.webp 600w, https://simonhearne.com/img/yZIs-oFPwg-900.webp 900w, https://simonhearne.com/img/yZIs-oFPwg-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/yZIs-oFPwg-600.jpeg 600w, https://simonhearne.com/img/yZIs-oFPwg-900.jpeg 900w, https://simonhearne.com/img/yZIs-oFPwg-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/yZIs-oFPwg-600.jpeg" width="1200" height="425" /></picture></a><figcaption>Abridged <a href="https://www.webpagetest.org/result/201018_DiK0_062c01875d889fe3168bbf1dec82a676/1/details/#waterfall_view_step1">WebPageTest</a> waterfall showing that over 50 objects load before LCP on the earlier filmstrip (LCP line added)</figcaption></figure>
<p>Delivering fast documents is one of the most important things you can do to improve page speed, if you can reduce this by 100ms then every other duration metric will get 100ms faster! Fast document delivery comes down to a few key optimisations:</p>
<ul>
<li><strong>DNS</strong> - DNS is the address book of the internet. Optimise DNS by increasing TTL and using a globally distributed DNS service (<a href="https://kinsta.com/blog/reduce-dns-lookups/">See this blog post for more detail</a>).</li>
<li><strong>TCP</strong> - The constraining factor in establishing a TCP connection is (normally) the round-trip time between user and server. Use a content delivery network to bring this as low as possible.</li>
<li><strong>TLS</strong> - Secure websites require one or more additional round-trips to create a secure connection. Ensure you have OCSP stapling enabled on the site's certificate (maybe even downgrade from EV to OV to achieve this!) and that your server or CDN is configured for TLSv1.3 support (see <a href="https://istlsfastyet.com/">Is TLS Fast Yet?</a>).</li>
<li><strong>TTFB</strong> - The time to first byte for your website is constrained by how fast the server can create the response. If possible, this should be cached in a CDN or reverse proxy. If HTML caching is not possible (for example if there is personalisation in the page), ensure that your server environment is able to deliver pages within 100ms.</li>
<li><strong>HTML</strong> - It may sound obvious, but the size and structure of your HTML document is critical to performance. Ensure that the HTML document is compressed and under 50kB over the network. Pay attention to the <code><head></code> of the document to ensure that the <code><title></code> is first and that there are no blocking third-party <code><script></code> tags here.</li>
</ul>
<p>Once the HTML is downloaded, the browser parses the document line-by-line to find resources in the critical path. CSS and JavaScript in the head are given very high priority, then images in the body are downloaded in the order they appear. If the browser parser sees a blocking script tag (i.e. no <code>async</code> or <code>defer</code> attribute) it will stop everything it is doing while fetching, parsing and executing that script. As such, scripts should always be either async, when order of execution is not important, or deferred when order of execution is important. It also makes sense to review inline scripts to reduce their impact.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/async_defer.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/recvBUK365-600.avif 600w, https://simonhearne.com/img/recvBUK365-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/recvBUK365-600.webp 600w, https://simonhearne.com/img/recvBUK365-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/recvBUK365-600.jpeg 600w, https://simonhearne.com/img/recvBUK365-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/recvBUK365-600.jpeg" width="900" height="406" /></picture></a><figcaption>Defer scripts where possible. <a href="https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html">More details.</a></figcaption></figure>
<p>Where possible, split your JavaScript bundle by page and by modern JavaScript support. This will allow you to send the smallest possible bundle and use modern technologies where supported. Note that <code>module</code> has a defer behaviour by default:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/app-homepage.esm.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">nomodule</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/app-homepage.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>The next step in the critical path is loading stylesheets: without CSS the browser does not know how to render the page so it will block render on any <code><link rel="stylesheet"></code> tags. Ensure that CSS is bundled by page to reduce unnecessary weight, you can fetch and cache a full stylesheet later in the page lifecycle using the media="none" hack (ensuring that this file won't cause a layout shift!):</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><br /> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy.css<span class="token punctuation">"</span></span><br /> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>none<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">onload</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value javascript language-javascript"><span class="token keyword">this</span><span class="token punctuation">.</span>media<span class="token operator">=</span><span class="token string">'all'</span></span><span class="token punctuation">"</span></span></span> <span class="token punctuation">></span></span></code></pre>
<p>LCP is measured independent of cache state, so ensure your static assets (JavaScript, CSS, images and fonts) can be cached in the browser for at least am hour with a response header like <code>cache-control: max-age: 3600</code>. Also make sure your text assets are compressed with gzip or brotli!</p>
<p>A common issue I see, especially on ecommerce sites, is a large number of non-critical images that load early in the page, such as in a mega menu definition. Native lazy-loading is a great technique to optimise LCP by reducing bandwidth contention during page load. The <code>loading</code> attribute is not yet supported in Safari, but it <a href="https://bugs.webkit.org/show_bug.cgi?id=196698">is in WebKit</a> and currently available in iOS Safari behind a flag, so we can expect general support soon! It <em>is</em> supported in all browsers which send data to CrUX, so implement now to benefit the data which will drive the Page Experience SEO update.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>menu-img.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><br /> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>200<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>200<span class="token punctuation">"</span></span><br /> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span> <span class="token punctuation">></span></span><br /><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hero-img.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><br /> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1024<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>600<span class="token punctuation">"</span></span><br /> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>eager<span class="token punctuation">"</span></span> <span class="token punctuation">></span></span></code></pre>
<p>Ensure your hero element has a <code>loading='eager'</code> attribute and that images below the fold or hidden by default have a <code>loading='lazy'</code> attribute. This simple optimisation allows the browser to give priority to downloading important assets sooner, improving LCP and user experience. Read more on <a href="https://web.dev/browser-level-image-lazy-loading/">web.dev</a>.</p>
<p>It should go without saying, but if your hero element is an image or a video then it should be delivered in the most optimised format for the browser. This may mean that a third-party service like Cloudinary or Akamai Image and Video Manager is a good option to dynamically optimise your media content.</p>
<p>The hero element should not be JavaScript-bound, so image carousels and embedded video players are best replaced with static images and native <code><video></code> elements with an appropriate <code>poster</code> image attribute. If you have a video poster, make sure that it is optimised and use a valid <code>preload</code> attribute like <code>none</code> or <code>metadata</code> to reduce bandwidth contention during page load. Read more about these optimisations on <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video">Mozilla Developer Network.</a></p>
<blockquote class="info"><p>A rule of thumb is that a hero image should not be larger than 200kB.</p></blockquote>
<h3 id="first-input-delay-fid" tabindex="-1">First Input Delay (FID) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#first-input-delay-fid" aria-hidden="true">#</a></h3>
<p>FID is a measure of how long the browser was busy with other tasks before it could react to a discrete user input event like a tap or click. This is an indication of how responsive the UI feels to a user, and how busy the CPU is with JavaScript processing.</p>
<p>The only consistent way to improve FID without degrading the user experience is to reduce the time spent executing JavaScript, both in the page load and during the page lifecycle. A simple way to game this metric would be to hide your content (perhaps behind a loading screen / spinner) until your JavaScript has completed executing, so your visitors don't try to interact before your app is completely ready. This will most probably negatively impact your LCP and CLS metrics though, so tread with caution!</p>
<p>Assuming we're trying to improve FID <strong>and</strong> improve visual performance, we only have a few options:</p>
<ul>
<li>Delay or remove third-parties</li>
<li>Defer non-critical scripts</li>
<li>Improve JS performance</li>
</ul>
<p>The first task is to run a performance trace on a key page to see where the main thread time is consumed. Any big chunks are cause for concern and should be investigated.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/tbt1.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/fv4pfMrJ9U-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/fv4pfMrJ9U-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/fv4pfMrJ9U-600.jpeg" width="600" height="420" /></picture></a><figcaption>Performance summary shows where time was spent, and total blocking time</figcaption></figure>
<p>Focus on <strong>long tasks</strong>, these are singular blocks of execution which are most likely to cause high first input delays. Click on a long task to reveal the call stack in the "Bottom-Up" tab below:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/tbt2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/Lg9qhvfO0U-600.avif 600w, https://simonhearne.com/img/Lg9qhvfO0U-900.avif 900w, https://simonhearne.com/img/Lg9qhvfO0U-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Lg9qhvfO0U-600.webp 600w, https://simonhearne.com/img/Lg9qhvfO0U-900.webp 900w, https://simonhearne.com/img/Lg9qhvfO0U-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/Lg9qhvfO0U-600.jpeg 600w, https://simonhearne.com/img/Lg9qhvfO0U-900.jpeg 900w, https://simonhearne.com/img/Lg9qhvfO0U-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Lg9qhvfO0U-600.jpeg" width="1200" height="739" /></picture></a><figcaption>Selecting a long task shows what code is responsible</figcaption></figure>
<p>You can group the view by URL to quickly determine which scripts are causing the issues, in this case the <code>cookielaw</code> banner SDK is responsible for 200ms of 'self' time, meaning that the time was spent in this script rather than due to calls made by it. This could be due to the script itself being slow, or the implementation being poor resulting in multiple duplicated function calls. Time to start digging!</p>
<p>Another potential cause for high FID is the hydration process. If you have a server-side rendered single page application, the hydration process adds the client-side application functionality to your static HTML. Whilst this process results in a faster first render (and LCP), it may delay interactivity and increase the potential for delay when a user tries to interact. If you are using SSR with client-side hydration <a href="https://twitter.com/addyosmani">Addy</a> has a <a href="https://addyosmani.com/blog/rehydration/">brief write-up</a> of the issue with a few resources on how to manage this performance gap.</p>
<h3 id="cumulative-layout-shift-cls" tabindex="-1">Cumulative Layout Shift (CLS) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#cumulative-layout-shift-cls" aria-hidden="true">#</a></h3>
<p>CLS is a measure of how stable the UI feels as the user loads and interacts with a page. It is a sum of unexpected layout shifts during the page lifecycle, like when an advertisement banner loads and pushes the main content of the page around.</p>
<figure>
<a href="https://simonhearne.com/images/core-web-vitals/cls1.gif">
<img src="https://simonhearne.com/images/core-web-vitals/cls1.gif" alt="animated gif of layout shift causing user to press the wrong button" loading="lazy" width="400" height="310" style="max-width:400px" /></a>
<figcaption>Layout shifts make for bad web experiences</figcaption>
</figure>
<p>Layout shift scores are derived from the impact the shift has on the viewport: a product of how much of the viewport changes and how far the element moves. A perfect cumulative score is zero, 0.1 is good. See the <a href="https://web.dev/cls/">official documentation</a> for more details.</p>
<p>Unexpected layout shifts mean that they do not occur immediately after a discrete user interaction, so it will likely have a negative impact on user experience.</p>
<p>CLS is measured by CrUX throughout the lifecycle of a page, from the start of the navigation right through to when the user leaves the page. All unexpected layout shifts are summed throughout this time and the total score is used for the measurement; this makes it a tricky metric to measure in a lab setting. This also means that tools such as <a href="https://www.webpagetest.org/">WebPageTest</a> and <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">mPulse</a> will report the best-case scenario for CLS, they will only collect this data whilst the page is loading and ignore further shifts such as those that occur during scroll.</p>
<p>Optimising CLS during page load is reasonably simple, we just need to prevent layout shifts! There are multiple causes for layout shifts, so let's look at some of the causes and how to prevent them occuring.</p>
<ul>
<li><strong>Web fonts</strong> - match your web font character and line spacing to the fallback font</li>
<li><strong>Ads</strong> - pre-allocate layout slots for ads, use a fallback image if ads fail or are blocked</li>
<li><strong>Late-loading CSS</strong> - ensure layout-critical CSS is in the critical path</li>
<li><strong>Images</strong> - always add a width and height attribute for images, so the browser can allocate space before the image downloads</li>
<li><strong>Dynamic content</strong> - where possible, pre-allocate layout space for dynamic elements</li>
</ul>
<p>Chrome's developer tools now has a handy method to identify what elements caused a layout shift, and how they contributed to your cumulative layout shift score</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/layoutshift1.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/tzzPddpM_I-600.avif 600w, https://simonhearne.com/img/tzzPddpM_I-900.avif 900w, https://simonhearne.com/img/tzzPddpM_I-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/tzzPddpM_I-600.webp 600w, https://simonhearne.com/img/tzzPddpM_I-900.webp 900w, https://simonhearne.com/img/tzzPddpM_I-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/tzzPddpM_I-600.jpeg 600w, https://simonhearne.com/img/tzzPddpM_I-900.jpeg 900w, https://simonhearne.com/img/tzzPddpM_I-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/tzzPddpM_I-600.jpeg" width="1200" height="708" /></picture></a><figcaption>Layout shifts are highlighted in the performance profile</figcaption></figure>
<p>If you hover on the layout shift in the experience track, Chrome will highlight the element on the page. Note that the details show you where the element moved from and to.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/layoutshift2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/W8w_iwcJ7V-600.avif 600w, https://simonhearne.com/img/W8w_iwcJ7V-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/W8w_iwcJ7V-600.webp 600w, https://simonhearne.com/img/W8w_iwcJ7V-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/W8w_iwcJ7V-600.jpeg 600w, https://simonhearne.com/img/W8w_iwcJ7V-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/W8w_iwcJ7V-600.jpeg" width="900" height="305" /></picture></a><figcaption>Hover on a shift to highlight the offending element</figcaption></figure>
<p>I recommend setting your network and CPU throttling options in the Performance tab, this makes it more likely that you will catch the layout shifts that happen in the wild due to network race conditions.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/layoutshift3.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/r2pjfQfitp-600.avif 600w, https://simonhearne.com/img/r2pjfQfitp-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/r2pjfQfitp-600.webp 600w, https://simonhearne.com/img/r2pjfQfitp-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/r2pjfQfitp-600.jpeg 600w, https://simonhearne.com/img/r2pjfQfitp-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/r2pjfQfitp-600.jpeg" width="900" height="183" /></picture></a><figcaption>Slow the profile down to make issues more obvious</figcaption></figure>
<p>Once you've identified the elements that are causing layout shifts, it's time to determine how to reduce or prevent them.</p>
<figure>
<a href="https://simonhearne.com/images/core-web-vitals/layoutshift3.gif">
<img src="https://simonhearne.com/images/core-web-vitals/layoutshift3.gif" alt="animated gif showing layout shifts on CNN article headline" loading="lazy" width="1038" height="356" style="max-width:700px" /></a>
<figcaption>Layout shifts on a CNN headline</figcaption>
</figure>
<p>In this example from CNN, the headline element shifts multiple times during page load. The major shift is due to the author's image loading and pushing the byline to the right. Another shift is caused by the web font for the headline changing the headline layout. The image shift can be resolved simply by adding width and height attributes to the <code><img></code> element!</p>
<p>For the text element shift, the font-face has a <code>font-display:swap</code> descriptor (which is a <a href="https://developers.google.com/web/updates/2016/02/font-display">good thing</a>!) but the fallback font has a different character width to the web font. When the web font is loaded, the text element shifts as the characters are more condensed and the element is sized to fit the contents. In this case it is a small layout shift but the impact on body text can be much greater, as text overflow may cause the body element to resize. It <em>might</em> be possible to fix this by matching the system font to the web font, using <a href="https://www.chromestatus.com/feature/5651198621253632">font-face descriptors</a>, but the most robust way to avoid font-based layout shifts is to preload the webfont and use <code>font-display: optional</code>. This combination gives the browser the best chance to have the webfont(s) available when it needs them, but lets the browser use the fallback font in case the webfont is not available. This ensures that there will be no layout shift due to fonts, both for the initial page load and subsequent page loads which will use the now cached webfont.</p>
<h2 id="other-vitals" tabindex="-1">Other Vitals <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#other-vitals" aria-hidden="true">#</a></h2>
<p>Whilst the initial Page Experience update defines LCP, FID and CLS as the <em>core</em> web vitals, these may change over time! There are a host of other metrics which provide additional value and are worth tracking & optimising. Below are some more key metrics that you might want to track and improve.</p>
<div class="resp-cols-3">
<img src="https://simonhearne.com/images/core-web-vitals/fcp_ux.svg" width="269" height="235" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/tti_ux.svg" width="269" height="235" loading="lazy" />
<img src="https://simonhearne.com/images/core-web-vitals/tbt_ux.svg" width="269" height="235" loading="lazy" />
</div>
<h3 id="first-contentful-paint-fcp" tabindex="-1">First Contentful Paint (FCP) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#first-contentful-paint-fcp" aria-hidden="true">#</a></h3>
<p>Whilst LCP measures the largest paint on the screen, FCP measures when the first paint occurs. This is an important metric for a number of reasons, not least that this is the first time a user knows that their navigation is actually working. This timing point will also correlate well with when the user's browser switches context from the previous page.</p>
<p>FCP is often identical to first paint, or very shortly after. First paint includes non-visible elements in the calculation, whereas FCP only measures content which is visible to the user. In this case, the site logo and header:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/core-web-vitals/firstpaint.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/sMVFRHM5sn-600.avif 600w, https://simonhearne.com/img/sMVFRHM5sn-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/sMVFRHM5sn-600.webp 600w, https://simonhearne.com/img/sMVFRHM5sn-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/sMVFRHM5sn-600.jpeg 600w, https://simonhearne.com/img/sMVFRHM5sn-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/sMVFRHM5sn-600.jpeg" width="900" height="496" /></picture></a><figcaption>First Paint and First Contentful Paint often occur together</figcaption></figure>
<h3 id="time-to-interactive-tti" tabindex="-1">Time to Interactive (TTI) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#time-to-interactive-tti" aria-hidden="true">#</a></h3>
<p>Time to interactive (TTI) is an approximation of when the page will feel interactive if a user tries to interact. This timing point is measured between the First Contentful Paint and when main thread has been free of long tasks for at least five seconds. Getting this metric as low as possible will ensure that your users have a great experience when they try to interact with your pages. Use TTI with TBT to get an overall picture of how busy the browser is when loading your pages.</p>
<h3 id="total-blocking-time-tbt" tabindex="-1">Total Blocking Time (TBT) <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#total-blocking-time-tbt" aria-hidden="true">#</a></h3>
<p>Total blocking time (TBT) is a measure of how busy the browser's CPU was while loading the page, measured as the sum of long tasks (less 50ms each) between FCP and TTI. Reducing this time will likely improve user experience and may help improve perceived performance too. Look for large long tasks in your JavaScript profiles and try to reduce, remove or delay them.</p>
<h2 id="how-to-track-performance" tabindex="-1">How to track performance <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#how-to-track-performance" aria-hidden="true">#</a></h2>
<p>Google's tools such as PageSpeed Insights, Lighthouse and <a href="http://web.dev/">web.dev</a> will all give you a measurement of your core web vitals. The 'field' data has some limitations though: the data is collected only from Google Chrome users who are opted in to anonymous usage statistics collection, and is aggregated monthly with a week or two delay. So if it's the first week of October, you may only have data from the whole month of August available.</p>
<p>If you want to track web vitals more closely, look into a real user measurement solution like <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">Akamai mPulse</a>. RUM tools can collect performance data from every browser that supports them and give you real time insight into how your performance is tracking. You can also quickly spot issues with specific pages or devices, making the data actionable.</p>
<h2 id="conclusion-and-actions" tabindex="-1">Conclusion & Actions <a class="direct-link" href="https://simonhearne.com/2020/core-web-vitals/#conclusion-and-actions" aria-hidden="true">#</a></h2>
<p>The proposed page experience update will most likely occur in mid-2021. This will have an impact on SEO ranking as well as eligibility criteria for Top Stories in Google SERPs. Google has proposed three new performance metrics which will be used as signals for this SEO update, with selection based on their adjacency to user-perceived performance and the ability to collect the data in the field.</p>
<p>Optimising for these performance metrics, as measured by Google in CrUX, might have a positive impact on ranking in the future, but will certainly have a positive impact on user experience. <a href="https://wpostats.com/">We know</a> that faster experiences lead to lower bounce rate, higher session length, better satisfaction scores, increased conversion, increased SEO traffic, increased revenue... So why wait?!</p>
<p>The great thing about performance optimisations is that many can be achieved with relatively minor tweaks to code, so go ahead and test your key pages, spot room for improvement and make positive change.</p>
The Psychology of Speed2020-10-15T00:00:00Zhttps://simonhearne.com/2020/psychology-of-speed/<p><lite-youtube videoid="7i_yQyHdxUo" playlabel="Play: The Psychology of Speed"></lite-youtube></p>
<p><a class="link-button no-link" href="https://simonhearne.com/presentations/psych-speed/#/">View Slides</a></p>
Giving Great (Remote) Software Demos2020-06-15T00:00:00Zhttps://simonhearne.com/2020/great-demos/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#introduction" aria-hidden="true">#</a></h2>
<p>I've been in technical pre-sales for about ten years, and software demos have been a part of my role since day one. I'm still working on perfecting my demos, but I do have some tips on getting the best results from the short time you have with your clients.</p>
<p>Key takeaways for those in a hurry:</p>
<ul>
<li>demo to the <em>people</em> in the meeting</li>
<li>no demo should be the same as another</li>
<li>start with the "aha!" moment and work back</li>
<li>only show the features that matter to the customer</li>
<li>always prepare for a strong five minute open and close</li>
</ul>
<p>Technical sales / solution engineering / pre-sales is (in my humble opinion) one of the best roles you can have in tech. It combines technical skill, creativity and sales skills and is critical to the success of complex software sales.</p>
<p>Demonstrating software is one of the most fun and high-impact tasks for a pre-sales engineer. You have the opportunity to use your expertize and experience to deliver a powerful, customized demo which could be the difference between a sale and a lost opportunity!</p>
<p>It is easy in life to settle into routines, and we are all guilty of giving the same demo a few times. Spending a little time to think more about your customer and your demo can result in a more engaging, more enjoyable and more successful meeting. In the following sections we will walk through some key steps to help achieve this.</p>
<h2 id="preparing-for-the-demo" tabindex="-1">Preparing for the Demo <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#preparing-for-the-demo" aria-hidden="true">#</a></h2>
<p>No matter what demo you're giving, preparation is key to success. Some of this can feel a bit nosey or intrusive, just remember that the goal is to give you background information which can help direct the demo.</p>
<h3 id="know-the-customer" tabindex="-1">Know the Customer <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#know-the-customer" aria-hidden="true">#</a></h3>
<p>Your sales representative will have done some work here already, but there are a few steps that you should take as a pre-sales engineer, and it only takes a few minutes to get a feel for the company:</p>
<ul>
<li>Do they have a tech blog? Take a look at what they've been publishing lately.</li>
<li>Are they a public company? Take a look at the CEO's message in the latest annual report.</li>
<li>What is the output of their marketing? Take a look at Twitter & LinkedIn to see what they are talking about.</li>
</ul>
<h3 id="know-the-audience" tabindex="-1">Know the Audience <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#know-the-audience" aria-hidden="true">#</a></h3>
<p>Note that this is separate from knowing the customer, this is about getting to know the individuals you will be working with. You should have a meeting agenda with the email addresses of all attendees, hopefully at least a few days before the demo! Book half an hour in your diary before the meeting to do some research on Twitter, LinkedIn, blogs etc.</p>
<p>Researching the attendees is not meant to be creepy, and it is unlikely that you would (or should) use anything that you learn explicitly in the call. Understanding the background of the attendees will help you spot opportunities, focus the demo and prepare for obvious objections.</p>
<h3 id="know-the-technology" tabindex="-1">Know the Technology <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#know-the-technology" aria-hidden="true">#</a></h3>
<p>This entirely depends on the solution that you are demoing, but it is important to know the technology in place. If your solution is web-based, use a tool like <a href="https://builtwith.com/">BuiltWith</a> to see what is being used already. This allows you to prepare for objections, technical complications and good synergies with other products.</p>
<p>You will need to do a bit more preparation if you are in a competitive position with an incumbent supplier. Make sure you know who you are competing with and your relevant strengths and weaknesses. Be prepared to <a href="https://simonhearne.com/2020/great-demos/#handling-objections">handle objections</a> based on the competitive solution(s).</p>
<h2 id="know-your-demo" tabindex="-1">Know your Demo <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#know-your-demo" aria-hidden="true">#</a></h2>
<p>There are three software demos you are likely to give, in order of the maturity of the opportunity:</p>
<p><strong>The Qualification</strong> - "Ooh shiny!"</p>
<p><strong>The Confirmation</strong> - "This solves all of my problems!"</p>
<p><strong>The Proof</strong> - "Now I've seen it with my own data, here's my credit card!"</p>
<p>It is important to be mindful of which stage the <strong>customer audience</strong> is at, and give the correct demo accordingly. Are you giving the same demo for all three stages? No worries, most of us do. Differentiating between these, even it's only the first few screens that change, can turn a low-engagement demo into an "I can spare another thirty minutes for this, please carry on" demo. You could be months into an engagement with many demos delivered, but it's likely that you need to give a qualification demo if you have to present to someone new to the company or the solution.</p>
<p>Whatever the demo, ensure that you follow tell-show-tell for each feature you include:</p>
<ul>
<li>Tell them what you're going to show them and what problem it solves</li>
<li>Show them how your solution solves their problem</li>
<li>Tell them what you showed them!</li>
</ul>
<h3 id="the-qualification-demo" tabindex="-1">The Qualification Demo <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-qualification-demo" aria-hidden="true">#</a></h3>
<p>You have two goals in the qualification demo:</p>
<ol>
<li>Create enough interest in the customer to progress to a proof of concept (or close the deal, if you're lucky!)</li>
<li>Qualify that your software actually meets the customer's requirements</li>
</ol>
<p>We often forget the second point in our enthusiasm for our solutions. Both sides should be evaluating whether this is a good fit, just like in a job interview.</p>
<p>It is tempting to jump straight into talking about <em>you</em>: your company, your product. It is critical to understand the customer's pain before showing any software screens at all. Push back if you have been invited to a meeting by a salesperson without any qualification or background, your management and leadership should definitely support you here. It is the salesperson's job to qualify opportunities and share that information with you.</p>
<p>It is quite likely that at some point you will find yourself in a meeting with a new customer without any background information. This is not your fault, and can be recovered: start the meeting with questions. This is actually an opportunity to build a trusted advisor role earlier in the relationship than normal, although obviously not ideal overall. Stick to <strong>open</strong> questions about their business, with a leaning towards the problems that your software solves:</p>
<ul>
<li>"How do you solve challenge X at the moment?"</li>
<li>"How successful do you feel that is?"</li>
<li>"How important is X to the business?"</li>
<li>"If you could change anything about X, what would it be?"</li>
</ul>
<p>In fact, do this even if you do have a good qualification document from the salesperson. Why wouldn't you ask these questions, two heads are better than one and there's a good chance that the answers you receive may give you more information. These questions have three effects on the rest of the meeting:</p>
<ul>
<li>It demonstrates that you care about the customer</li>
<li>It gives the customer an opportunity to discuss their challenges with a technical advisor</li>
<li>It helps to build rapport and establish two-way communication</li>
</ul>
<p>All of this helps to avoid the dreaded <em>radio broadcast demo</em>, where the customer sits in near-silence for an hour or more. This can happen remote or in-person, and is awkward for both sides!</p>
<p>Once you have built rapport and understood or confirmed the qualification, it's time to demo! Remember the goals for a qualification demo are two-fold: show the customer that it solves their problem, and qualify your software does actually solve their problem.</p>
<p>Think back to your last qualification demo where the customer had an "aha!" moment. What was the first screen of your software you showed? Now think to the "aha!" moment, what screen were you showing then?</p>
<p>We often have a number of linear demo flows we follow, building "bridges" from customer challenge to software solution. In fact, the cover for the (very good) pre-sales book <a href="https://www.amazon.com/Demonstrating-Win-Indispensable-Complex-Products/dp/0615477097/?tag=webnin-21">Demonstrating to Win!</a> is a bridge!</p>
<p>Whilst this is great in a software training session or a late stage demo, it can be cumbersome in the qualification demo. Remember the screen from the "aha!" moment? What was it showing? Show that first. Use this as the conversation starter, and build a flow from here to key features based on the feedback from the audience. It can feel awkward at first, but try giving a five minute walk-through of a single screen, no clicking around! Once you have talked through this screen, confirm that the customer understands what you've shown them and ask if they have any questions. Leave the silence to hang for 20 - 30s, this allows people time to think and un-mute. It's important to get feedback at this point, if no-one asks any questions, turn it back on the audience and ask a leading question to a friendly in the audience by name. For example, you might ask "Jane, is this what you were expecting to see? What do you think would be most important to see next?"</p>
<p>During the demo, take note of where the audience appears most engaged or asks the most questions. This will help to build your closing summary, and should help you build your business case.</p>
<h3 id="the-confirmation-demo" tabindex="-1">The Confirmation Demo <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-confirmation-demo" aria-hidden="true">#</a></h3>
<p>The confirmation demo may be combined with the qualification demo, depending on the complexity of your software and the maturity of the opportunity.</p>
<p>Think about your goals for a confirmation demo. If you offer free trials or proofs of concept, the goal of this demo is to <em>skip the free trial</em>. If you can sell your software without a trial you will save time, cost and Salesforce administration! Other CRMs are available 🙂</p>
<p>I would expect an opportunity to be in a reasonably mature stage by the time you are preparing a confirmation demo. This means that this demo should prove to the customer that you know what they need, and that your software solves those requirements. This means that you should be investing the majority of your time on this demo, and you should have the following prepared:</p>
<ul>
<li>Attendees and roles for the demo</li>
<li>Personal needs for each attendee</li>
<li>A list of key requirements (perhaps from an RFP or procurement)</li>
<li>The name of the decision maker</li>
</ul>
<p>This is a lot of information, and a lot of work to prepare. The majority of this should be provided by your sales representative, but you can (and should) reach out to folks who will be on the demo to ask them questions about what they would like to see, what problems they hope that you can solve and what their role is in the purchasing decision.</p>
<p>A demo flow comes quite naturally once you have this information prepared. Don't start your demo with a demo, but a summary of this information. For example you might say:</p>
<blockquote>
<p>Thank you everyone for speaking with me over the past few days. It seems to me that the critical requirement is xxx, that the selected solution should also provide yyy. Does that sound right?</p>
</blockquote>
<p>Then let the silence hang for 10 - 20 seconds. It will feel uncomfortable, but it is really important to get confirmation at this point. You will also find out who is your 'friendly' from the customer at this point, make a mental note of their first name and use it once or twice in the demo to ask for confirmation. This makes you part of the team, and helps to break down the sales / prospect barrier.</p>
<p>Once you have confirmation of the requirements, now we can show a single screenshot / screen of the solution. It might be worth preparing a few, in case your assumptions above are wrong, but the key is to have a single screenshot which showcases <strong>your solution solving the critical requirement.</strong></p>
<p>Talk through this screenshot and discuss how it solves their requirement. Once you are satisfied that you have spoken to their requirement, ask them for confirmation. If they agree, ask them what else they would like to see. Once you have noted everything that the audience mentions, quickly build a demo flow based on their requirements. Then for each requirement, take the shortest possible route in your software to show how you solve the requirement. If your software requirement cannot solve a requirement, start with a very brief objection handling statement to qualify the requirement:</p>
<blockquote>
<p>"I hadn't heard that requirement before, is that something we need to cover off now or can we take it offline?"</p>
</blockquote>
<p>Once you have gone through a requirement, confirm with the requestor that you have satisfied them. Asking this question can come off as combative, especially if it is phrased such as "Does that answer your question" or "Is that what you wanted to see". It is possible to take the edge off the question by adding humility:</p>
<blockquote>
<p>"I hope that answers your question", "it seems to me that should meet your requirements, would you agree".</p>
</blockquote>
<h3 id="the-proof" tabindex="-1">The Proof <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-proof" aria-hidden="true">#</a></h3>
<p>This is my favorite demo. It's normally the culmination of weeks or months of work, has lots of pressure and lots of attendees! It is your opportunity to show off how great your solution is, and how well you can meet the customer's requirements.</p>
<p>By this time in the sales process, you should know exactly what the customer needs to see in order to confirm that your solution is the right fit for their challenges, it makes sense then that your demo shows these features and nothing else. If you only need to show 10% of your features to show the deal, only show those 10% of features! There is strength in simplicity, and value in closing a meeting early if you can.</p>
<p>While I suggest that you should keep the demo as short as possible, it is critical to make sure that the customer knows what they're about to see and how it solves their problem. People have bad memory, so you'll also need to recap it at the end. A good structure for this demo might look like:</p>
<ul>
<li>Introductions
<ul>
<li>Include the names and roles of everyone in the meeting. If you don't know someone, politely ask who they are and what their interest is in the solution: "Hi Joe Blogs, I don't believe we have met before? What would you like to see in today's demo?"</li>
</ul>
</li>
<li>Background of all of the customer's relevant challenges
<ul>
<li>Specific challenges</li>
<li>Solution requirements</li>
<li>Potential value of solving the challenges</li>
</ul>
</li>
<li>Challenge 1
<ul>
<li>More background of the customer's challenge ("you told me that...")</li>
<li>Specific capabilities of your solution which would solve this challenge</li>
<li>Value of solving the challenge (hours saved, $ gained, insight provided)</li>
<li>Shortest path to demo the capability</li>
<li>Confirmation - "would you agree that this solves your challenge?"</li>
</ul>
</li>
<li>Challenge 2, 3, 4...</li>
<li>Summary
<ul>
<li>Confirmation of challenges - "we heard from you that..."</li>
<li>Confirmation of solutions - "we have you shown that..."</li>
<li>Confirmation of understanding - "is there anything I have missed or could make more clear?"</li>
<li>Confirmation of intent - "based on this information, it seems that our solution meets your requirements"</li>
</ul>
</li>
</ul>
<p>I've seen a lot of these demos start with a few slides about the pitching company's history, position in the market, how amazing they are etc. Why? This meeting is about your customer, not you. Leave that out, or have some slides in your pocket in case you are challenged on your ability to deliver. Otherwise, keep <em>everything</em> in the demo about your customer.</p>
<h2 id="pitfalls" tabindex="-1">Pitfalls <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#pitfalls" aria-hidden="true">#</a></h2>
<p>With a customer-driven demo, there are a few pitfalls to avoid. It becomes much easier to handle these once you know how to recognize them.</p>
<h3 id="the-rabbit-hole" tabindex="-1">The rabbit hole <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-rabbit-hole" aria-hidden="true">#</a></h3>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/rabbit-hole.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/YFdn1lpLH5-600.avif 600w, https://simonhearne.com/img/YFdn1lpLH5-900.avif 900w, https://simonhearne.com/img/YFdn1lpLH5-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/YFdn1lpLH5-600.webp 600w, https://simonhearne.com/img/YFdn1lpLH5-900.webp 900w, https://simonhearne.com/img/YFdn1lpLH5-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/YFdn1lpLH5-600.jpeg 600w, https://simonhearne.com/img/YFdn1lpLH5-900.jpeg 900w, https://simonhearne.com/img/YFdn1lpLH5-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/YFdn1lpLH5-600.jpeg" width="1200" height="359" /></picture></a><figcaption>Alice goes down the rabbit hole to Wonderland</figcaption></figure>
<p>The rabbit hole is when we spend a disproportionate amount of time on a small feature of the solution or customer challenge. This can come from either the demonstrator or the customer, more commonly from a confused or belligerent customer. It's also possible if you have a favorite or 'safe' feature which you are comfortable spending time in, or if you have a particular affinity for a specific feature.</p>
<p>The escape route is relatively simple, once you have identified that you're in the rabbit hole. If you need to leave a feature, just ask a closed leading question of the audience: "I'm conscious that we have a limited amount of time to cover a lot of requirements, is it ok if we move on and come back if we have time at the end?".</p>
<h3 id="the-drunkards-walk" tabindex="-1">The drunkards walk <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-drunkards-walk" aria-hidden="true">#</a></h3>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/random-walk.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/qxBLVIlxUL-600.avif 600w, https://simonhearne.com/img/qxBLVIlxUL-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/qxBLVIlxUL-600.webp 600w, https://simonhearne.com/img/qxBLVIlxUL-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/qxBLVIlxUL-600.jpeg 600w, https://simonhearne.com/img/qxBLVIlxUL-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/qxBLVIlxUL-600.jpeg" width="900" height="615" /></picture></a><figcaption></figcaption></figure>
<p>Often a symptom of a lack of information before a demo, the drunkards walk is a seemingly random jump from feature to feature. This will end up confusing the customer and making your solution seem more complex than it is.</p>
<p>When giving the demo, don't be tempted away from a feature until you have given a summary. If a customer asks about something that will take you away from your current flow, take a note and let the customer know you'll get to it:</p>
<blockquote>
<p>"Great question! That is covered in another part of the solution, I've taken a note to cover that when we get to it. Please remind me if I miss it out!"</p>
</blockquote>
<h3 id="the-harbour-tour" tabindex="-1">The harbour tour <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-harbour-tour" aria-hidden="true">#</a></h3>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/tsp.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/owPT6FDdNQ-600.avif 600w, https://simonhearne.com/img/owPT6FDdNQ-900.avif 900w, https://simonhearne.com/img/owPT6FDdNQ-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/owPT6FDdNQ-600.webp 600w, https://simonhearne.com/img/owPT6FDdNQ-900.webp 900w, https://simonhearne.com/img/owPT6FDdNQ-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/owPT6FDdNQ-600.jpeg 600w, https://simonhearne.com/img/owPT6FDdNQ-900.jpeg 900w, https://simonhearne.com/img/owPT6FDdNQ-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/owPT6FDdNQ-600.jpeg" width="1200" height="562" /></picture></a><figcaption></figcaption></figure>
<p>"This is what you've been training for!" you think as you prepare your demo, a checklist of the full feature set of your software ready in front of you.</p>
<p>It is incredibly tempting to try to show every single feature of your product in a demo. The logic is pretty solid: if you show every feature then the customer will understand how powerful the solution is! Unfortunately it doesn't work out that way, a harbour tour of your software - moving from feature to feature - will likely have the opposite effect. It is often hard to tell a story with your software during the harbour tour, making the transition from one feature to the next jarring for the customer. Demonstrating every feature can also lead to quite a dry demo, and may even give the impression that your solution is overly complex, or does more than the customer could ever need.</p>
<p>The harbour tour is differentiated from the runaway train and the drunkards walk as it is, in most respects, a great demo! The challenge here is knowing when to stop. Remember to focus on the minimum viable demo, what are the key features which will close the deal? Let the customer tell you if you've missed anything that is important to them, rather than throwing everything against the wall and seeing what sticks.</p>
<h3 id="the-runaway-train" tabindex="-1">The runaway train <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-runaway-train" aria-hidden="true">#</a></h3>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/runaway_train.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/3TB0r9W9H2-600.avif 600w, https://simonhearne.com/img/3TB0r9W9H2-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/3TB0r9W9H2-600.webp 600w, https://simonhearne.com/img/3TB0r9W9H2-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/3TB0r9W9H2-600.jpeg 600w, https://simonhearne.com/img/3TB0r9W9H2-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/3TB0r9W9H2-600.jpeg" width="900" height="463" /></picture></a><figcaption>The runaway train doesn't stop until it gets where it's going</figcaption></figure>
<p>Probably the most common pitfall in technical demos, the runaway train is the well-rehearsed step by step walk-through that has been delivered hundreds of times before, which stops for nothing! This is generally delivered by veterans who have honed their demos over time, or newbies who are following a safe demo path (and likely the product training). A robotic demo routine can be both dull and fragile, customer questions can throw you off your tracks and disrupt your flow for the rest of the demo.</p>
<p>The solution for the runaway train pitfall is simple, let the customer drive the demo! Ask confirmation questions after each feature and get feedback as often as possible. It can be hard to break this pattern, so try practicing with a colleague: ask them to be the customer and 'drive' the demo, then follow their lead. Once you've done this a few times it will feel even more natural than your well-rehearsed demos!</p>
<h3 id="the-endless-objection" tabindex="-1">The endless objection <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#the-endless-objection" aria-hidden="true">#</a></h3>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/columbo.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/XHDv0nPCeD-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/XHDv0nPCeD-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/XHDv0nPCeD-600.jpeg" width="600" height="250" /></picture></a><figcaption></figcaption></figure>
<p>Some customers are just there to make your life difficult! If you have a member of the audience who constantly throws objections (or will not let an objection go) there are two potential solutions.</p>
<p>If the objections are valid and following a theme, you need to <a href="https://simonhearne.com/2020/great-demos/#handling-objections">address it</a> as soon as you can to prevent it derailing your demo. You can do this by acknowledging the objection and defer the responsibility to another time: "great question, I think it would be best for me to follow up by email with a complete answer. Would that be ok for everyone?". By using the word "everyone" in the confirmation question you put the pressure of the objection back on the person raising it, it is their decision to use everyone's time on this question. It also leaves an opening for someone else on the call to confirm the objection, in that case it really is important to handle it in the demo!</p>
<h2 id="screen-sharing-hygiene" tabindex="-1">Screen-sharing Hygiene <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#screen-sharing-hygiene" aria-hidden="true">#</a></h2>
<p>You are likely to be sharing your screen in either remote or in-person demos. Whilst the technology to do this is generally robust, it can open you up to some unnerving possibilities.</p>
<figure>
<video autoplay="" muted="" loop="" playsinline="" poster="https://simonhearne.com/images/great-demos/dnd.png" style="max-width:500px" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/dnd.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/dnd.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/dnd.mov" type="video/mov" />
</video>
<figcaption>Enable Do Not Disturb mode on MacOS from the notification center</figcaption>
</figure>
<p>In all cases, it makes sense to mute your notifications. On MacOS this is as simple as <span class="key">⌥</span> + click on the notification center icon. Windows makes this a little more tricky - you need to head to control panel to disable notifications.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/windows_dnd.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/vX6RqBopG9-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/vX6RqBopG9-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/vX6RqBopG9-600.jpeg" width="600" height="274" /></picture></a><figcaption>Disable Windows notifications in Notifications Settings</figcaption></figure>
<p>One of the most cringe-inducing moments of a demo is the "can you see my screen?" awkward beginning. Save yourself some time and frustration by getting prepared to make this silky smooth. If the screen sharing software is not your normal software, e.g. if the customer has shared theirs, make sure you practice sharing your screen before the meeting. If you are on MacOS Catalina, you will be familiar with the screen sharing permissions issue. This new permissions setting means that new apps are not allowed to record your screen or audio until you allow them, and allowing it might require restarting the app! You will know how frustrating this is if you've had this happen to you on a call with a customer. Thankfully there are a couple of ways to avoid this nightmare.</p>
<p>The first trick is to avoid the native applications! Almost every video conferencing product supports joining via the web, in order to be compatible with Linux and ChromeOS. They might hide this ability, but if you look hard enough you will find it 🙂 The benefit is that it will inherit your browser's permissions, so you only need to give the permissions once.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/join_from_browser.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/gmvV3Arbmq-600.avif 600w, https://simonhearne.com/img/gmvV3Arbmq-900.avif 900w, https://simonhearne.com/img/gmvV3Arbmq-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/gmvV3Arbmq-600.webp 600w, https://simonhearne.com/img/gmvV3Arbmq-900.webp 900w, https://simonhearne.com/img/gmvV3Arbmq-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/gmvV3Arbmq-600.jpeg 600w, https://simonhearne.com/img/gmvV3Arbmq-900.jpeg 900w, https://simonhearne.com/img/gmvV3Arbmq-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/gmvV3Arbmq-600.jpeg" width="1200" height="306" /></picture></a><figcaption>Look for a 'join from browser' link</figcaption></figure>
<p>The second tip is to proactively give permission. The app you have to download might not ask for permission until you attempt to share your screen, and you might not be able to share your screen until your meeting starts! In MacOS, head to System Preferences -> Security & Privacy -> Privacy -> Screen Recording and enable the app.</p>
<figure class="no-shadow">
<img src="https://simonhearne.com/images/great-demos/accessibility_settings.png" alt="" loading="lazy" maxwidth="500" />
<figcaption>Preemptively give apps Screen Recording & Microphone access</figcaption>
</figure>
<p>Once you are able to share your screen, let's make sure that starting your screen share is as smooth as possible. If you are presenting from a slide deck, make sure you are in presenter mode <em>before</em> you share your screen, this makes it easier to share the correct app or screen and will make the process more smooth.</p>
<p>There are a few methods to avoid the dreaded "can you see my screen?" intro. Most simply, agree with another person on the call (from your company) that they will let you know when they can see your screen, ideally via an internal chat tool that you can see from your phone. A slightly more involved option is to join the meeting twice, once as a presenter and once as an attendee. This way you will see exactly what the customer sees! There are obvious drawbacks here, especially if you have limited bandwidth available, but there's nothing like seeing the customer view to give you confidence about what you are presenting. I do this for very important meetings or webinars with an iPad on my desk, so I can see at a glance what the presentation looks like.</p>
<h3 id="screen-sharing-with-multi-monitor-setups" tabindex="-1">Screen-sharing with Multi-monitor Setups <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#screen-sharing-with-multi-monitor-setups" aria-hidden="true">#</a></h3>
<p>Screen-sharing becomes quite simple with multi-monitor setup. Use your secondary monitor for the demo / presentation and share the whole screen. This means that you can switch between apps without having to re-share them.</p>
<p>If you have to switch between screens or windows, have these staged and ready to go. On MacOS I recommend using multiple desktops - this allows you to easily switch between full screen apps using a three-finger swipe or <span class="key">control</span> + <span class="key">→</span>. On Windows, have your apps lined up in the task switcher so you can quickly move from one to the other.</p>
<h3 id="screen-sharing-with-single-monitor-setups" tabindex="-1">Screen-sharing with Single-monitor Setups <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#screen-sharing-with-single-monitor-setups" aria-hidden="true">#</a></h3>
<p>Sharing your screen becomes a little tricky if you only have one monitor. If you are physically plugging in to a projector or TV - you can avoid this by setting it as a second monitor, not mirrored! If you're presenting remotely you will have to make do with the one monitor you have. A good option is to only share the application you need to. In many cases, you can spend 100% of your time in a browser window. Use a web-based presentation tool (Google Slides or HTML!) and get your tabs set up in the order that you will need them. Use <span class="key">⌘</span> + <span class="key">[n]</span> to switch tabs where [n] is the tab index, i.e. <span class="key">⌘</span> + <span class="key">1</span> will switch to the left-most tab.</p>
<p>If you have to share your whole screen, for example if you need to switch between multiple applications, make sure your workspace is as clear as possible. In MacOS, multiple desktops are your friend here, just make sure your other applications (email, IM etc.) are minimized so you don't accidentally flash them to your customer when you confuse swiping left for swiping right!</p>
<h3 id="high-resolution-displays" tabindex="-1">High Resolution Displays <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#high-resolution-displays" aria-hidden="true">#</a></h3>
<p>If you have a newer laptop or a nice high-definition external monitor, you will likely have a horizontal resolution between 3,000 and 4,000 pixels. While this is great for productivity, it can be a nightmare for screen-sharing. If your customer is showing the display on a 720p projector, or a window on their 1080p display, your monitor will be scaled back significantly. If we take the 720p example, each projected pixel will have to squeeze in nine pixels from your 4k monitor. Ever heard customers say that it's hard to read text from your screen? This could well be why. Sharing at 4k might also consume significant resources on your machine as it tries to compress the information to share it over the internet.</p>
<p>There are three options to resolve this issue. No matter what route you take, it's worth confirming with your customer that they can read the content you are sharing without straining their eyes.</p>
<ol>
<li>Zoom in! This is the simple option, most apps will respond to <span class="key">⌘</span> + <span class="key">+</span> or <span class="key">control</span> + <span class="key">+</span> to zoom in.</li>
<li>Share a window. Instead of sharing your full 4k screen, share an application window which is scaled to a quarter of your display.</li>
<li>Change your monitor resolution. This is something that often happens automatically when you connect to a projector, but not in remote presentations. In MacOS you can change the scaling in display settings, and in Windows you should be able to change the display resolution in Control Panel.</li>
</ol>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/macos_resolution.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/bLEGy7UHpj-600.avif 600w, https://simonhearne.com/img/bLEGy7UHpj-900.avif 900w, https://simonhearne.com/img/bLEGy7UHpj-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/bLEGy7UHpj-600.webp 600w, https://simonhearne.com/img/bLEGy7UHpj-900.webp 900w, https://simonhearne.com/img/bLEGy7UHpj-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/bLEGy7UHpj-600.jpeg 600w, https://simonhearne.com/img/bLEGy7UHpj-900.jpeg 900w, https://simonhearne.com/img/bLEGy7UHpj-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of macos resolution settings" loading="lazy" decoding="async" src="https://simonhearne.com/img/bLEGy7UHpj-600.jpeg" width="1200" height="473" /></picture></a><figcaption>Adjust your effective resolution in Display preferences</figcaption></figure>
<h3 id="sharing-your-browser" tabindex="-1">Sharing your browser <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#sharing-your-browser" aria-hidden="true">#</a></h3>
<p>It is quite likely that you will be sharing your browser window at some point in a demo. This can be a bit risky! There are two options to reduce how much information is shared when you share your browser. The first is to have a separate user profile for your demos. Most browsers support multiple profiles which isolate bookmarks, credentials and extensions. The challenge can be making sure you have all of the credentials set up correctly in the demo profile!</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/chrome_profiles.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/lIB_HLaSo0-575.avif 575w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/lIB_HLaSo0-575.webp 575w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/lIB_HLaSo0-575.jpeg" width="575" height="617" /></picture></a><figcaption>Separate your Chrome profiles by use</figcaption></figure>
<p>It is also a good idea to tidy up your browser before sharing your screen. This can be as simple as making sure your 'new tab' page is a blank page, and hiding your bookmarks and extension bars. You can quickly toggle the bookmark bar in Chrome on MacOS with <span class="key">⌘</span> + <span class="key">⇧</span> + <span class="key">b</span>. Your extension bar should have a grab handle so you can shrink it. Removing these distractions allows your audience to focus on what you're showing them, and prevents you from revealing anything you shouldn't.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/great-demos/tidy_chrome.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/r73cIgMaXe-600.avif 600w, https://simonhearne.com/img/r73cIgMaXe-900.avif 900w, https://simonhearne.com/img/r73cIgMaXe-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/r73cIgMaXe-600.webp 600w, https://simonhearne.com/img/r73cIgMaXe-900.webp 900w, https://simonhearne.com/img/r73cIgMaXe-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/r73cIgMaXe-600.jpeg 600w, https://simonhearne.com/img/r73cIgMaXe-900.jpeg 900w, https://simonhearne.com/img/r73cIgMaXe-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/r73cIgMaXe-600.jpeg" width="1200" height="295" /></picture></a><figcaption>Remove distracting bookmarks and extension icons</figcaption></figure>
<h2 id="ending-a-demo" tabindex="-1">Ending a Demo <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#ending-a-demo" aria-hidden="true">#</a></h2>
<p>How you end a demo will influence your customer's impression more than you might expect. A good ending cements the messages you want to convey in the demo and will be remembered more than the other 55 minutes. This is the logic behind tell-show-tell:</p>
<ul>
<li>Tell them what you're going to show them</li>
<li>Show them</li>
<li>Tell them what you showed them</li>
</ul>
<p>The demo is only there to confirm the points that you have pitched at the beginning. The end, then, is to summarize those points and confirm understanding with the audience.</p>
<p>A weak ending puts the whole demo at risk, so make sure you are prepared to make it concise and impactful. The following four points should give a good structure to a ~2.5 minute close before the sales representative takes over:</p>
<ul>
<li>"At the beginning of the call we said..."</li>
<li>"During the demo you have seen how we can solve your challenges..."</li>
<li>"Is there anything else you were hoping to see in this demo?"</li>
<li>"Would you say that this solution solves your challenges?"</li>
</ul>
<p>Hopefully the customer responds with a yes to the last question! Don't be afraid to wait 10-20s for an answer. If the silence hangs too heavy, you can follow up with an open, leading question to give the customer an opportunity to share any objections:</p>
<blockquote>
<p>"it seems like you are not 100% sure that the solution solves your challenges, what have we not shown you today that could get you to 100%?"</p>
</blockquote>
<p>This closes the demo with the customer agreeing that they need your solution, rather than you telling them they do. This is subtle but important - there is a subconscious difference between being told that the solution is good, vs. having to make the statement yourself. The worst case scenario with this ending is that the customer tells you what you have to hit first in the follow-up demo 😃</p>
<p>Finally, take stock of any objections and follow-up actions you have noted down. Make sure each has an owner from your side and the customer side, and follow up with this by email shortly after the call.</p>
<p>Having your demo structure written down in front of you, or in speaker notes, is a great way to ease in to a new demo approach. It will become second nature surprisingly quickly.</p>
<h2 id="handling-objections" tabindex="-1">Handling objections <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#handling-objections" aria-hidden="true">#</a></h2>
<p>Often viewed as a challenge, objections are actually great opportunities to learn more about your customer and their challenges. When I say objections here, I mean any question that challenges what you are presenting, for example "can your solution do this?" or "that looks complex to me". There could be a whole book on the subject, but the sections below cover the high-level steps required to handle objections.</p>
<h3 id="1-acknowledge-the-objection" tabindex="-1">1 - Acknowledge the objection <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#1-acknowledge-the-objection" aria-hidden="true">#</a></h3>
<p>The first thing you should do is to look after the ego of the person who raises the objection. This is often accomplished with a simple acknowledgement, and a statement of empathy:</p>
<blockquote>
<p>"Great question!"</p>
</blockquote>
<h3 id="2-confirm-understanding" tabindex="-1">2 - Confirm understanding <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#2-confirm-understanding" aria-hidden="true">#</a></h3>
<p>This step is often skipped, but can really help to clarify the objection. It also gives the audience member an opportunity to take their objection back or even answer it for themselves!</p>
<blockquote>
<p>"It sounds like you're asking if..."</p>
</blockquote>
<h3 id="3a-provide-the-answer" tabindex="-1">3a - Provide the answer <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#3a-provide-the-answer" aria-hidden="true">#</a></h3>
<p>In the most simple of scenarios, the question is easy to answer. In this case it is tempting to skip steps 1 & 2 and give an instant answer, but this misses the opportunity for the customer to answer their own question or, even better, someone else from the audience to answer it for you!</p>
<p>When giving the answer, make sure you directly address the objection:</p>
<blockquote>
<p>"The solution provides feature X which directly resolves objection Y. It seems to me that this answers your question, would you agree?"</p>
</blockquote>
<p>Asking the customer to agree serves two purposes: confirming that you have actually resolved the objection, and making the customer state that you have resolved the objection.</p>
<h3 id="3b-defer-the-answer" tabindex="-1">3b - Defer the answer <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#3b-defer-the-answer" aria-hidden="true">#</a></h3>
<p>You will not have all the answers in all cases. It is perfectly fine to say "I don't know"!. What the customer really wants in this case is for you to have acknowledged and understood the question, and have a route to answering it in the future:</p>
<blockquote>
<p>"I don't know the answer to that question, I will take a note and get back to you after the call. Does anyone else know the answer?"</p>
</blockquote>
<p>It's important to ask the audience if anyone else can answer the question, ideally someone from the customer side will! This also saves you a bit of work in follow-up.</p>
<h3 id="3c-admit-defeat" tabindex="-1">3c - Admit defeat <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#3c-admit-defeat" aria-hidden="true">#</a></h3>
<p>This is the most awkward objection, and the one where your response is most important. The question to which you don't have the <em>right</em> answer. This could be missing functionality in your solution, inability to deliver in a certain time-frame or any other negative response.</p>
<p>There are two steps to admitting defeat. The first is to be up-front and confirm the requirement:</p>
<blockquote>
<p>"Our product does not do that. Is this a critical requirement for you?"</p>
</blockquote>
<p>If they don't really care about it, then you can acknowledge it and move on. If they do care about it, you need to take an action:</p>
<blockquote>
<p>"I appreciate that you have a critical requirement for the solution to do X. I will take a note to speak to the product owner and see if it is on the road map"</p>
</blockquote>
<p>Make sure that the action is noted down and be sure to follow up after the call.</p>
<h2 id="wrapping-up" tabindex="-1">Wrapping up <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#wrapping-up" aria-hidden="true">#</a></h2>
<p>In the introduction I said that spending a little time to think more about your customer and your demo can result in a more engaging, more enjoyable and more successful meeting. In this post we have discussed how important it is to invest in preparing for a demo, doing research on the company, the competition and the audience.</p>
<p>We then looked at the three most important demo types: the Qualification, the Confirmation and the Proof. Knowing which demo you need to give is a great way to improve your demos, and will leave your customers feeling more engaged with you and your solution.</p>
<p>Knowing the common pitfalls for demos makes it easier to spot them, and we introduced some simple techniques to overcome these pitfalls. This gives you the power to recover a less than perfect demo early on in the call, and turn it around into the kind of demo that leaves you and the customer feeling great.</p>
<p>Remote presentations require a little more thought and preparation than in-person meetings. We looked at some simple screen-sharing hygiene to make sure you don't spend the first few minutes arranging windows, or get interrupted half-way through by an inappropriate Slack message!</p>
<p>We know how disappointing it is when a movie has a weak ending, the same is true for demos! It is easy to get carried away and run out of time, before you know it you're at two minutes past the end of the meeting and folks are starting to drop off the call. Ensuring that you prepare a few minutes for a strong close will leave a lasting positive impression with the audience.</p>
<p>One of the biggest fears most people have in a demo is the "tough question". The objection handling steps we outlined will help you to buy time, win favor and leave the audience satisfied with your answer. No matter what question they ask!</p>
<p>We've only touched the surface of technical demos in this post, but hopefully there are some useful nuggets here for you. The following section lists some books that I have found particularly beneficial to improving my pre-sales skills, if you want to dig deeper! As a bit of a spoiler: all of these revolve around putting the customer first. Active listening, acknowledgement, mirroring and understanding are all key techniques to build trust. Without trust, it is unlikely that you will be successful.</p>
<h2 id="further-reading" tabindex="-1">Further reading <a class="direct-link" href="https://simonhearne.com/2020/great-demos/#further-reading" aria-hidden="true">#</a></h2>
<p>There are hundreds of books on this topic. The selection below are from my library, and each adds value to technical presenting. I've roughly prioritized them by how applicable they are to technical demos, they all have something to teach which can be valuable for pre-sales engineers.</p>
<p><strong><a href="https://www.amazon.com/Mastering-Technical-Sales-Professional-Development/dp/1608077446/?tag=webnin-21">Mastering Technical Sales: The Sales Engineer's Handbook</a></strong> by John Care</p>
<p>This book is a great resource for those new to technical pre-sales. It covers much more than software demos including RFPs, needs analysis and whiteboarding. There are three chapters on demonstrations, though, starting with one of my favorite quotes from pre-sales:</p>
<blockquote>
<p>The eager rush to demonstrate your product, which we’ll refer to in this chapter as the "dash to demo," often sounds the death knell for any sales opportunity</p>
</blockquote>
<p><strong><a href="https://www.amazon.com/Demonstrating-Win-Indispensable-Complex-Products/dp/0615477097/?tag=webnin-21">Demonstrating To Win!: The Indispensable Guide for Demonstrating Complex Products</a></strong> by Robert Riefstahl</p>
<p>Demo2Win is the golden standard for demonstrating complex software products. The book was originally published in 2000, but the second edition (2011) has been updated for remote presentations. The focus is on multi-hour demos for complex software, but most of the techniques are applicable to any software. This is where I first read about 'tell-show-tell'. Robert pitches demos as if they are movies: the intro has to capture attention and introduce the characters. The story has to follow an arc which introduces the challenge, the complication, the solution and the success. The ending has to wrap everything up and leave you speechless.</p>
<p><strong><a href="https://www.amazon.com/SPEED-TRUST-Thing-Changes-Everything/dp/1416549005/?tag=webnin-21">The Speed of Trust: The One Thing that Changes Everything</a></strong> by Stephen M. R. Covey</p>
<p>A technical pre-sales engineer should be a trusted advisor. This book discusses the importance of trust and how it is earned and lost. A good book for life in general, and quite applicable to software demonstrations.</p>
<p><strong><a href="https://www.mandel.com/landing-pages/scipab-pocket-prompt-ti">SCIPAB Pocket Reference</a></strong> by Mandel</p>
<p>This is not a book, but a quick reference to the SCIPAB methodology to start demos: <strong>S</strong>ituation, <strong>C</strong>omplication, <strong>I</strong>mplication, <strong>P</strong>osition, <strong>A</strong>ction and <strong>B</strong>enefit. This methodology is a valuable tool to help craft your ~two minute pitch before a demo.</p>
<p><strong><a href="https://www.amazon.com/Great-Demo-Stunning-Software-Demonstrations/dp/059534559X/?tag=webnin-21">Great Demo! How to Create and Execute Stunning Software Demonstrations</a></strong> by Peter Cohan</p>
<p>Great Demo! introduces the concept of showing the most powerful screen first, and has some good ideas around slides that you should prepare to top-and-tail a demo. The book is a little long for the actionable content, but the key concepts are solid.</p>
<p><strong><a href="https://www.amazon.com/Never-Split-Difference-Negotiating-Depended/dp/1847941494/?tag=webnin-21">Never Split the Difference: Negotiating as if Your Life Depended on It</a></strong> by Chris Voss</p>
<p>I think of this book like How to Win Friends and Influence People, but with more terrorists. Chris was a hostage negotiator and shares his experience and techniques to resolve negotiations successfully. One of the most valuable sections of the book is on how to find out what people really want, you can't negotiate until you know what you should be offering! This is of critical importance to delivering excellent demos and closing opportunities.</p>
<p><strong><a href="https://www.amazon.com/How-win-Friends-Influence-People/dp/8189297813/?tag=webnin-21">How to Win Friends and Influence People</a></strong> by Dale Carnegie</p>
<p>The classic book on communication strategies. While the book does not discuss software demonstrations in depth, it will help you spot your own flaws, tics and mannerisms. Once you are aware of your own issues, you can work on them to improve your communication and demo skills.</p>
<p><strong><a href="https://www.amazon.com/How-Talk-Kids-Will-Listen/dp/B007YZX6LE/?tag=webnin-21">How to Talk so Kids Will Listen... And Listen So Kids Will Talk</a></strong> by Adele Faber and Elaine Mazlish</p>
<p>This one is definitely the odd one out. I read it before having children (I can't remember why!) and I've referred back to it regularly since having kids. Working with children involves a lot of negotiation, objection handling and reading between the lines. Working with customers involves... a lot of the same!</p>
Who Opts-in to Save-data?2020-05-29T00:00:00Zhttps://simonhearne.com/2020/save-data/<h2 id="in-summary" tabindex="-1">In Summary <a class="direct-link" href="https://simonhearne.com/2020/save-data/#in-summary" aria-hidden="true">#</a></h2>
<p>Save-data is a client hint which indicates that your users want you to send less content to them. It is an option on Google Chrome Mobile and it's used predominantly in Asia, Africa and South America. There is a strong relationship between the value of a user's device and their likelihood to opt-in to save-data: the more expensive the device, the less likely they are to opt-in. While the opt-in rate is highest on cheaper devices and in less economically developed countries, this is not an absolute delineation. For example: users in Canada have an opt-in rate of over 34% (compared to ~7% in the US) and users on the latest Samsung flagship have an opt-in rate of almost 18% globally. Income inequality is a factor in national opt-in rate in most countries.</p>
<p>Google Chrome Mobile will use this hint to provide a proxied web experience in some scenarios, but this process is quite opaque. It may also defer scripts, force <code>font-display: swap</code> and force lazy loading of iframes and images.</p>
<p>Website owners should not rely on the browser to make these optimizations, but instead should allow users to opt-in to a data-saving experience. Save-data is a 'set-and-forget' setting which is rarely changed; providing a UI to allow users to opt-in or out of a 'lite' mode is a great progressive enhancement for modern websites.</p>
<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/save-data/#introduction" aria-hidden="true">#</a></h2>
<p>Save-data (often conflated with Data-Saver Mode / Chrome Lite Pages / <a href="https://support.google.com/chrome/answer/2392284?co=GENIE.Platform%3DAndroid&hl=en">Chrome Lite mode</a>) is a client preference for reduced data consumption. The hint is currently available to JavaScript via the Network Information API and as an HTTP request header. The preference is set in browser settings, with Chrome mobile suggesting that you enable it when you go first open the application:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/litemode.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/ltSaWVeVsG-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ltSaWVeVsG-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome recommending lite mode on mobile" loading="lazy" decoding="async" src="https://simonhearne.com/img/ltSaWVeVsG-600.jpeg" width="600" height="544" /></picture></a><figcaption>Chrome suggests enabling Lite Mode on first open</figcaption></figure>
<p>The save-data hint in itself does nothing, but there are a number of scenarios where service providers and website owners can react to the hint to deliver a better experience:</p>
<ul>
<li>Google Chrome may force interventions such as deferring external scripts and lazyloading iframes and images</li>
<li>Google Chrome may proxy requests to improve performance for users who have opted in to 'Lite Pages' <em>and</em> have a poor network connection</li>
<li>Website owners can deliver lighter versions of their applications, e.g. by lowering image quality, delivering a server-side rendered page or reducing the amount of third-party content</li>
<li>ISPs can transform HTTP images to reduce size on the last-mile</li>
</ul>
<p>There have already been a number of posts looking into Chrome Lite Pages, I'd recommend you take a look at Tim Kadlec's <a href="https://timkadlec.com/remembers/2019-03-14-making-sense-of-chrome-lite-pages/%5D(https://timkadlec.com/remembers/2019-03-14-making-sense-of-chrome-lite-pages/)">blog post</a> for a great introduction to the feature and the problem it is trying to solve. In this post we're going to focus on the users who choose to opt-in.</p>
<h2 id="collecting-data" tabindex="-1">Collecting Data <a class="direct-link" href="https://simonhearne.com/2020/save-data/#collecting-data" aria-hidden="true">#</a></h2>
<p>Akamai mPulse collects performance data from a range of sources in the browser, including the <a href="https://developer.akamai.com/mpulse/whats-in-a-beacon/#mobile-connection-details">Network Information API</a>. This API provides information about the device's current connectivity, as well as a <code>saveData</code> property. The open source <a href="https://github.com/akamai/boomerang/blob/master/plugins/mobile.js">mobile plugin</a> for boomerang provides access to the API, and the data is available in <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">Akamai mPulse</a> as well as the open source <a href="https://github.com/akamai/boomerang">boomerang</a>.</p>
<p>The data is only available in Chromium-based browsers which support Lite pages, for now. A quick review of the data retreived in May shows that only four browsers have a reported save-data rate above 0.1%:</p>
<table>
<thead>
<tr>
<th>Browser Name</th>
<th>Save-data Rate</th>
<th>Relative Traffic Share</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chrome</td>
<td>0.61%</td>
<td>26%</td>
</tr>
<tr>
<td>Chrome Mobile</td>
<td>28.14%</td>
<td>15%</td>
</tr>
<tr>
<td>Yandex Browser</td>
<td>2.92%</td>
<td>0.42%</td>
</tr>
<tr>
<td>Puffin</td>
<td>35.23%</td>
<td>0.003%</td>
</tr>
</tbody>
</table>
<p>We will be looking at Chrome Mobile hits in the following research, this represents the largest population of opted-in traffic and allows comparitive analysis of opted-in vs opted-out.</p>
<p>It is worth noting that this data is aggregated from mPulse beacons, which will show a natural weighting towards North America and Europe due to the customer base. The data is taken from a time period in May with a total of about two billion user experiences.</p>
<h2 id="by-country" tabindex="-1">By Country <a class="direct-link" href="https://simonhearne.com/2020/save-data/#by-country" aria-hidden="true">#</a></h2>
<p>This section reviews the save-data opt-in rate by country. Analysis by country allows us to compare the opt-in rate against a number of factors which are accessible by country, such as cost of data and average income. The opt-in rate is the percentage of pageviews which had the <code>saveData</code> property set to <code>true</code>. Scroll down to move through various dimensions of analysis.</p>
<div class="chart-sticky-container" id="sticky-container">
<div class="vega-chart loading chart-animate chart-sticky chart-active" id="vis3" data-spec="/data/save-data/scatter2.json">Loading chart...</div>
<div class="stage" data-stage="0">
<h3 id="average-cost-per-gb" class="no-link">Average Cost per GB</h3>
<p>
When considering why people might opt-in to save data, the first answer that came to mind was the cost of the data. If the cost of data is high, then you are more likely to try to reduce your data usage, you might assume. I pulled the latest data for the average price paid for 1GB of data, normalized to USD, and compared it with the save-data opt-in rate by country.
</p><p>
Looking at this first scatter plot we can see that there is no immediately obvious correlation between cost of data in a country and the opt-in rate. A positive correlation would look like the points forming a line diagonally from bottom-left to top-right.
</p><p>
The result do show a few interesting points. The strongest correlating factor for save-data appears to be continent. The split in the data at around 40% opt-in separates Asia, Africa and South America (with high opt-in rates) versus North America, Europe and Oceania. The exceptions to this are Japan in Asia, which creeps below 40% mark at just 34.1% and Mexico in North America which is well above 40% at 66.4%.
</p><p>
Another standout feature of this chart is India. It is high on the y-axis at 62% opt-in, but almost zero on the x-axis at an average cost of $0.09 per GB. I first thought this was an error but it turns out that data really is <a href="https://www.business-standard.com/article/pti-stories/india-mobile-internet-rates-remain-lowest-in-world-prasad-119120201245_1.html">that cheap</a> in India!
</p><p>
Whilst this chart does give us some interesting information about why people might opt-in to save data, it does not show the strong correlation that I had expected.
</p>
</div>
<div class="stage" data-stage="1">
<h3 id="average-individual-income" class="no-link">Average Individual Income</h3>
<p>
The separation between continents in the first chart led me to a second hypothesis, that save-data opt-in rate is correlated with the wealth of a country. I pulled the latest income per capita values available from the World Bank and plotted this against opt-in rate.
</p><p>
Whilst again there is a clear separation between continents, this has become more pronounced. The countries that are less likely to opt-in to save-data are clustered towards the right-hand-side of the chart, on the higher end of average personal income. The countries that are more likely to opt-in are fairly evenly spread across the x-axis, with a slight positive trend which disagrees with the hypothesis.</p><p>
UAE, at $43k GDP per capita, is near the right-hand-side of the chart but has a 64% opt-in rate. Compared this to Canada at $46k and 34% respectively.</p><p>
One country that exhibits strange behavior here is Ireland. At $79k GDP per capita it is one of the richest countries in the world, but the average income of $44k gives it a income / GDP ratio of 0.56, one of the lowest globally. Compare this to the Netherlands which has a similar income but lower GDP at $53k, making an income / GDP ratio of 0.83. This isn't necessarily related to save-data, but interesting nonetheless and is perhaps a symptom of the tax advantages for basing technology companies in the country.
</p><p>
Saudi Arabia shows that a high average individual income does not necessarily correlate with low save-data opt-in. We know the dangers of taking averages, and this may highlight the impact of wealth disparity amongst some countries.
</p>
</div>
<div class="stage" data-stage="2">
<h3 id="disposable-income" class="no-link">Income Inequality</h3>
<p>
This chart plots income inequality, as the percentage of total income earned by the bottom 50% of earners. A higher number represents a more even distribution of income.</p><p>
The plot shows that the countries where save-data opt-in rates are higher, have greater income inequality - or relatively a greater population of low earners. Here we can see that at just 7.8% of income going to the bottom earners, Saudi Arabia has a significant wealth divide. This helps to explain the high opt-in rate.
</p>
</div>
<div class="stage" data-stage="3">
<h3 id="hours-worked-for-1gb" class="no-link">Hours Worked for 1GB</h3>
<p>
Using the data collected above, we can calculate the average number of hours required to work to earn enough to purchase 1GB of data (assuming 2,000 working hours per year). This measure should get more close to the perceived cost of mobile data. My hypothesis is that the higher the relative cost of data in a country, the greater the likelihood for opt-in to save-data. This would look like a rough line from top-left to bottom-right (note that the x-axis is reversed).
</p><p>
The two obvious extremes here are Malawi on the far left and Israel on the far right of the chart. Malawi has an average annual income of $312 and an average data cost of $27 per GB, meaning that the average Malawian would have to work 256 hours to earn enough for 1GB of mobile data. In Israel, the average income is $36k and the average cost for 1GB is 11¢. This makes the hours required for one GB 0.006, just 22 seconds!
</p><p>
The left-hand-side of the chart is populated almost wholly by African countries, where salaries are low and data is expensive. The data generally agrees with the hypothesis, but does not show a direct correlation.</p>
</div>
<div class="stage" data-stage="4">
<h3 id="average-network-speed">Average Network Speed</h3>
<p>
So far we have looked primarily at financial motivators for saving data, which has shown a rough correlation between relative cost of data and the likelihood to opt-in to save-data. Another motivator might be network performance. The Chrome Lite Pages opt-on prompt promises up to 60% reduced data and faster page loads, if a user knows that their connection is poor they might be more likely to opt-in to Lite Mode, and thus save-data.</p><p>
The data shows that countries with an average mobile connection speed of less than 20Mbps have a much higher save-data opt-in rate, and countries with faster speeds are less likely to have high opt-in rates. One obvious outlier is Norway, with the highest average speed at 48.2Mbps but a high opt-in rate of 37%. On the other end of the spectrum, Belarus has an average speed of 7.7Mbps but an opt-in rate of 20%.
</p>
</div>
<div class="stage" data-stage="5">
<h3 id="4g-network-availability" class="no-link">4G Network Availability</h3>
<p>
Much like the download rate correlation, 4G availability shows little correlation with opt-in rate. In fact a stark difference can be observed between Ireland, at 63.7% 4G penetration and 25% opt-in versus The Netherlands with at 92.8% penetration and 25.4% opt-in rate.</p><p>
India is an outlier at 62% opt-in rate, but with one of the best 4G availability rates at 91%!
This indicates that 4G penetration on its own is not a critical factor in likelihood to opt-in to save-data.
</p>
</div>
</div>
<h2 id="by-device" tabindex="-1">By Device <a class="direct-link" href="https://simonhearne.com/2020/save-data/#by-device" aria-hidden="true">#</a></h2>
<p>We've filtered the results down to Chrome Mobile in this analysis, which limits us to Android devices. But Android is a diverse ecosystem: from the $50 Alcatel 1 through to the $2,500 Huawei Mate XS. Device specifications and network capability vary greatly across the spectrum of devices, meaning that simply looking at Chrome Mobile covers a diversity of capability.</p>
<p>The chart below shows the top devices in the dataset, from the top 25 manufacturers:</p>
<div class="chart-sticky-container" id="sticky-container">
<div class="vega-chart loading chart-sticky" id="vis4" data-spec="/data/save-data/manufacturers.json">Loading chart...</div>
<p>The results show that users of cheaper devices are more likely to opt-in to save-data. Take a look at Samsung devices for example. The highlighted devices are the <a href="https://www.gsmarena.com/samsung_galaxy_s9-8966.php">Galaxy S9</a>, the <a href="https://www.gsmarena.com/samsung_galaxy_a50-9554.php">Galaxy A50</a> and the <a href="https://www.gsmarena.com/samsung_galaxy_j7_prime-8314.php">Galaxy J7 Prime</a>. These phones represent three reasonably similar devices from Samsung across their flagship, premium and budget product lines respectively. The <strong>J</strong> range is marketed to younger or more cost-conscious users and has relatively low specifications. The <strong>A</strong> series devices tend to inherit the components of the <strong>S</strong> series with an ~18 month delay.</p><p>The J7 was released 1.5 years before the S9, almost three years before the collection of this data. A such, this is likely to be purchased at a discount or used by those on pre-paid connections. The A50 was released in 2020, the same year of this analysis, so is likely purchased as a good value premium device or offered as a contract upgrade. The S9 was a flagship device released at ~$800 one year prior to the analysis, so is likely owned by users on a high-cost contract which includes a large amount of included data.</p>
<p>The difference in save-data opt-in is stark: just <strong>17.7%</strong> for the premium S9 vs <strong>36%</strong> for the A50 and <strong>64%</strong> for the budget J7. Could this be users trying to keep their device <em>feeling</em> fast as it ages? Or budget-conscious users preserving small data budgets?</p>
<p>Another clear difference can be observed between premium and budget manufacturers. For example: Google devices average 21% opt-in, whilst Realme devices average 67%!</p>
</div>
<h2 id="by-connectivity" tabindex="-1">By Connectivity <a class="direct-link" href="https://simonhearne.com/2020/save-data/#by-connectivity" aria-hidden="true">#</a></h2>
<p>It is a fair assumption that folks on poorer connections are more likely to opt-in to save-data, so let's look at the data! In the same API that gives us the save-data preference, we can also get the browser's current estimated downlink speed and network latency. This data is rounded and sometimes randomized to prevent fingerprinting, but is generally a good indicator of network quality. Take a look at your current advertised data below:</p>
<div id="netinfo">
<pre>waiting...</pre>
</div>
<p>The <code>effectiveType</code> is derived from the <code>rtt</code> and <code>downlink</code> values, you can see the derivation in the <a href="https://wicg.github.io/netinfo/#effective-connection-types">API Documentation</a>. Perhaps confusingly, <code>4g</code> is effectively infinite speed - any connection faster than 0.7Mbps and 270ms RTT is given this effective type.</p>
<p>When we extract that data and plot it with the save-data opt-in rates we see very little correlation. The majority of experiences fall into the 4g bucket, with the <code>Cable/DSL</code> (e.g. home wifi) bucket showing a 21.7% average opt-in rate, vs. ~31.6% opt-in rate for <code>Dialup</code> and <code>Cellular</code>. There are a large number of hits in the <code>Not Set</code> connection group, these are from connections which cannot be classified through NetInfo or IP lookup.</p>
<p>The results imply that users who do not have a home wifi connection are more likely to use the save-data hint.</p>
<div class="vega-chart loading" id="vis5" data-spec="/data/save-data/sd_by_ect.json">Loading chart...</div>
<p>The differences here are much smaller than between devices, which is likely due to the fact that save-data is a set-and-forget setting. Users are unlikely to change it based on their connectivity.</p>
<h2 id="performance" tabindex="-1">Performance <a class="direct-link" href="https://simonhearne.com/2020/save-data/#performance" aria-hidden="true">#</a></h2>
<p>It may seem strange that I haven't mentioned performance results yet, given my interests! The reason is that it is impossible to separate correlation from causation with aggregated real user monitoring data. If a page is slow with save-data set, it is difficult to determine the cause of the slow performance. We do not know if any interventions are used by Chrome or the website.</p>
<p>To have a go at analyzing the data, we can separate out a single device and plot the performance of page loads with save-data set or not. The following chart is a five-number summary showing the 5th, 25th, 50th, 75th and 95th percentiles of DOM Ready time for the most common device in the dataset: the Samsung Galaxy S9. The results show that the value of th save-data flag makes zero appreciable difference to the performance, as measured by DOM Ready. This is as expected for the majority of sites which do not react to the hint, and thus deliver the same content. It is also unclear whether beacons are sent for cloud-rendered pages delivered by Google, so it is possible that these pages will be faster but not included in the data set.</p>
<div class="vega-chart loading" id="vis6" data-spec="/data/save-data/s9_perf.json">Loading chart...</div>
<h2 id="let-s-not-forget-lite-pages" tabindex="-1">Let's not Forget Lite Pages <a class="direct-link" href="https://simonhearne.com/2020/save-data/#let-s-not-forget-lite-pages" aria-hidden="true">#</a></h2>
<p>Lite pages are proixed through Google Servers to improve performance and reduce size over the network. Google will optimize non-secure images and force performance interventions on the users device (e.g. <a href="https://web.dev/native-lazy-loading/">lazy loading</a> images and iframes). It can also redirect you to a 'cloud rendered' version of the page...</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">Interesting. What's the analytics / reporting outcome if a Lite Page is served (cloud rendered)? Are the rendered versions cached? Will analytics scripts fire? Does the cloud browser have a unique UA? Can you track Lite -> full sessions?<br />I've got so many questions!</p>— Simon Hearne (@simonhearne) <a href="https://twitter.com/simonhearne/status/1145603877275873281?ref_src=twsrc%5Etfw">July 1, 2019</a></blockquote>
<p>You can try this for yourself (if you have a device with Chrome Mobile) by enabling "Lite Mode" in your settings and overriding effective connection type to 2G in <a href="chrome://flags">chrome://flags</a>:</p>
<div class="two-fig-cols">
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/lite-savings.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_3Gf9jfyOp-600.avif 600w, https://simonhearne.com/img/_3Gf9jfyOp-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_3Gf9jfyOp-600.webp 600w, https://simonhearne.com/img/_3Gf9jfyOp-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_3Gf9jfyOp-600.jpeg 600w, https://simonhearne.com/img/_3Gf9jfyOp-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of lite mode settings claiming it has saved 38MB out of 310MB downloaded" loading="lazy" decoding="async" src="https://simonhearne.com/img/_3Gf9jfyOp-600.jpeg" width="900" height="700" /></picture></a><figcaption>Chrome Lite Mode settings screen</figcaption></figure>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/ect-flag.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_ISH2vDpQc-600.avif 600w, https://simonhearne.com/img/_ISH2vDpQc-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_ISH2vDpQc-600.webp 600w, https://simonhearne.com/img/_ISH2vDpQc-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_ISH2vDpQc-600.jpeg 600w, https://simonhearne.com/img/_ISH2vDpQc-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of chrome flags option for connection type set to slow 3G" loading="lazy" decoding="async" src="https://simonhearne.com/img/_ISH2vDpQc-600.jpeg" width="900" height="700" /></picture></a><figcaption>ECT Flag set to Slow 2G</figcaption></figure>
</div>
<p>The logic as to when Chrome will serve a Lite Page is not necessarily consistent, and Lite Pages will not be served for authenticated sessions. I believe analytics tags will not fire on Lite Pages although I'm happy to see data showing otherwise, or indeed any information from Google on the matter.</p>
<p>You can use the <a href="https://developers.google.com/web/updates/2018/09/reportingapi">Reporting API</a> to see if your pages are being proxied by Chrome Lite pages. Set a <code>Report-To</code> endpoint and look out for a report with the type <code>intervention</code> and an id <code>LitePageServed</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br /> <span class="token property">"type"</span><span class="token operator">:</span><span class="token string">"intervention"</span><span class="token punctuation">,</span><br /> <span class="token property">"url"</span><span class="token operator">:</span><span class="token string">"https://yoursite.com/page"</span><span class="token punctuation">,</span><br /> <span class="token property">"body"</span><span class="token operator">:</span><span class="token punctuation">{</span><br /> <span class="token property">"id"</span><span class="token operator">:</span><span class="token string">"LitePageServed"</span><span class="token punctuation">,</span><br /> <span class="token property">"message"</span><span class="token operator">:</span><span class="token string">"Modified page load behavior on the page because the page was expected to take a long amount of time to load. https://www.chromestatus.com/feature/5148050062311424"</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h2 id="opting-out-of-save-data" tabindex="-1">Opting-out of Save-data <a class="direct-link" href="https://simonhearne.com/2020/save-data/#opting-out-of-save-data" aria-hidden="true">#</a></h2>
<p>You may find that users complain that your page is broken in Lite Mode, or you may just not like Chrome making this intervention on your behalf. Some pages do not render quite right, as can be seen below for the BBC.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/bbc-lite.jpg"><picture><source type="image/avif" srcset="https://simonhearne.com/img/yjtrdn9xX--600.avif 600w, https://simonhearne.com/img/yjtrdn9xX--900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/yjtrdn9xX--600.webp 600w, https://simonhearne.com/img/yjtrdn9xX--900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/yjtrdn9xX--600.jpeg 600w, https://simonhearne.com/img/yjtrdn9xX--900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of bbc loading with an odd layout in lite mode" loading="lazy" decoding="async" src="https://simonhearne.com/img/yjtrdn9xX--600.jpeg" width="900" height="1088" /></picture></a><figcaption>BBC Lite looking worse for wear</figcaption></figure>
<p>Developers can opt-out of cloud-rendered Lite Pages by including the <code>cache-control: no-transform</code> response header. This should prevent any intermediary (e.g. ISP, Chrome, CDN) from modifying the response, but it is a heavy-handed approach. <code>no-transform</code> may also interfere with CDN optimizations such as HTML minification and dynamic script injection.</p>
<h2 id="next-steps" tabindex="-1">Next Steps <a class="direct-link" href="https://simonhearne.com/2020/save-data/#next-steps" aria-hidden="true">#</a></h2>
<h3 id="getting-the-data" tabindex="-1">Getting the data <a class="direct-link" href="https://simonhearne.com/2020/save-data/#getting-the-data" aria-hidden="true">#</a></h3>
<p>Before doing anything about save-data, you need to know how many of your current users are sending the client hint. You can collect this data in your analytics tool of choice, for example with Google Analytics:</p>
<pre><code>ga('create', 'UA-XXXX-Y', 'auto');
ga('set', 'savedata', navigator.connection.saveData);
ga('send', 'pageview');
</code></pre>
<p>Preferably, collect this data with your performance analytics tool. You can configure this easily in the mPulse UI:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/cdim.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_6qFJvfzl7-600.avif 600w, https://simonhearne.com/img/_6qFJvfzl7-900.avif 900w, https://simonhearne.com/img/_6qFJvfzl7-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_6qFJvfzl7-600.webp 600w, https://simonhearne.com/img/_6qFJvfzl7-900.webp 900w, https://simonhearne.com/img/_6qFJvfzl7-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_6qFJvfzl7-600.jpeg 600w, https://simonhearne.com/img/_6qFJvfzl7-900.jpeg 900w, https://simonhearne.com/img/_6qFJvfzl7-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of mPulse UI configuration of custom dimension" loading="lazy" decoding="async" src="https://simonhearne.com/img/_6qFJvfzl7-600.jpeg" width="1200" height="637" /></picture></a><figcaption>mPulse Custom Dimensions are configured in the UI</figcaption></figure>
<h3 id="reacting-to-the-hint" tabindex="-1">Reacting to the hint <a class="direct-link" href="https://simonhearne.com/2020/save-data/#reacting-to-the-hint" aria-hidden="true">#</a></h3>
<p>If more than 1% of your users are sending the save-data hint, it makes sense to do something about it. This can be anything from delivering lighter images through your CDN to delivering a static version of your site.</p>
<p>Images are the biggest source of transferred bytes on most web pages, so are the obvious candidate for saving data.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/img-weight.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/1xr3x9qKrC-600.avif 600w, https://simonhearne.com/img/1xr3x9qKrC-900.avif 900w, https://simonhearne.com/img/1xr3x9qKrC-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/1xr3x9qKrC-600.webp 600w, https://simonhearne.com/img/1xr3x9qKrC-900.webp 900w, https://simonhearne.com/img/1xr3x9qKrC-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/1xr3x9qKrC-600.jpeg 600w, https://simonhearne.com/img/1xr3x9qKrC-900.jpeg 900w, https://simonhearne.com/img/1xr3x9qKrC-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="http archive chart showing growth of images delivered to mobile, from 75kb in 2011 to 910kb in 2020" loading="lazy" decoding="async" src="https://simonhearne.com/img/1xr3x9qKrC-600.jpeg" width="1200" height="741" /></picture></a><figcaption>HTTP Archive - Image Growth. <a href="https://httparchive.org/reports/state-of-javascript?start=earliest&end=latest&view=list">Source</a></figcaption></figure>
<p>As we have seen earlier in the analysis, save-data is not limited to low-end devices. This means that you are likely sending 2x or 3x images to devices which have opted-in to save-data. A simple change to reduce size would be to only send 1x images to devices with save-data. If you have a CDN or image delivery service, it is likely possible to automate this! For example, Cloudinary can <a href="https://cloudinary.com/blog/the_holy_grail_of_image_optimization_or_balancing_visual_quality_and_file_size#automatic_quality_default_values">automatically</a> reduce image quality if the save-data hint is set to true. Akamai <a href="https://developer.akamai.com/akamai-image-and-video-manager">Image Manager</a> can even reduce quality based on network connectivity, with endless possibilities for custom behaviors based on the save-data client hint.</p>
<p>One of the biggest bottlenecks on the web is JavaScript. If your site is one of the 50% that ship more than 417kB of JS, sending a page without it will likely be a far superior experience for users with limited connectivity and under-powered devices.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/save-data/js-weight.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_TEGxMjIRK-600.avif 600w, https://simonhearne.com/img/_TEGxMjIRK-900.avif 900w, https://simonhearne.com/img/_TEGxMjIRK-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_TEGxMjIRK-600.webp 600w, https://simonhearne.com/img/_TEGxMjIRK-900.webp 900w, https://simonhearne.com/img/_TEGxMjIRK-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_TEGxMjIRK-600.jpeg 600w, https://simonhearne.com/img/_TEGxMjIRK-900.jpeg 900w, https://simonhearne.com/img/_TEGxMjIRK-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="http archive chart showing growth of javascript delivered to mobile, from 50kb in 2011 to 417kb in 2020" loading="lazy" decoding="async" src="https://simonhearne.com/img/_TEGxMjIRK-600.jpeg" width="1200" height="728" /></picture></a><figcaption>HTTP Archive - JavaScript Growth. <a href="https://httparchive.org/reports/state-of-javascript?start=earliest&end=latest&view=list">Source</a></figcaption></figure>
<p>Do you need to ship your whole framework for a landing page?</p>
<p>For even more creative ideas on this topic, I highly recommend watching Tim Vereecke's talk <a href="https://www.youtube.com/watch?v=X0BbSvSbwD0">Data-S(h)aver Strategies</a> from London Web Performance in 2019:</p>
<p><lite-youtube videoid="X0BbSvSbwD0" playlabel="Play: Data-S(h)aver Strategies"></lite-youtube></p>
<p>Something that Tim recommends is to allow users to opt-out of the lite mode without having to disable save-data. This can be achieved with a UI toggle and a cookie which overrides the save-data preference. This option, like dark theme toggles, allows users to choose the experience that best suits their current situation. It is quite possible that a large number of users opted-in to save-data when they first configured Chrome Mobile and have not thought about it since, and that an equally large number of users ignored the intial prompt and don't know that the feature exists.</p>
<h2 id="conclusion" tabindex="-1">Conclusion <a class="direct-link" href="https://simonhearne.com/2020/save-data/#conclusion" aria-hidden="true">#</a></h2>
<p>We have seen that there can be many reasons for users to opt-in to save-data, and that the performance delivered to devices doesn't (yet) vary much based on the hint. A lack of awareness from both developers and web users mean that a potentially valuable web platform feature is under utilized. We currently rely on Chrome to make interventions on our behalf, potentially resulting in broken layouts, missing analytics and an unknown result with ad impressions. Could this be because Chrome is the only browser to embrace Save-Data?</p>
<p>Mindful observation of the save-data hint can provide a better user experience and save your visitors' data budgets. If you're not reacting to it now, think about what can be done to proactively reduce the data sent to your users.</p>
<script defer="" src="https://simonhearne.com/js/save-data-charts.js"></script>
Measuring Performance behind consent popups2020-05-13T00:00:00Zhttps://simonhearne.com/2020/testing-behind-consent/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#introduction" aria-hidden="true">#</a></h2>
<p>Cookie consent has given users control over the data that is shared with websites and third-parties, but it has made measuring performance difficult. Cookie consent is managed outside of standard APIs, so it can be implemented any way a website wants. Within the EU (and generally for websites hosted in the EU) consent is not assumed, so a visitor must opt-in to third-party tracking etc.</p>
<p>This makes testing performance difficult as standard synthetic test tools such as WebPageTest will only measure the <em>opted-out</em> experience. Conversely, real user measurement (RUM) tools such as mPulse will only measure the <em>opted-in</em> experience. See the problem? This also means that large-scale datasets such as <a href="https://httparchive.org/">HTTP Archive</a> and Google's <a href="https://developers.google.com/web/tools/chrome-user-experience-report">CrUX</a> present the performance of the opted-out and opted-in experience, respectively.</p>
<p>The problem with RUM data is generally unavoidable, as performance measurement is not a functional requirement, although few websites have so far implemented a true opt-in solution for performance tracking. For reference, mPulse provides the <a href="https://developer.akamai.com/tools/boomerang/docs/tutorial-howto-opt-out-or-opt-in.html">inline consent plugin</a> as a simple method to implement this correctly.</p>
<p>The problem of synthetic tests is thankfully more simple to solve: we simply need to have the test 'opt-in' as a user would. Once we have the ability to opt-in, there will be multiple scenarios we can explore:</p>
<ul>
<li><strong>Opted-out first view</strong> - this is the easy one</li>
<li><strong>Opted-out repeat view</strong> - cached experience</li>
<li><strong>Opted-in first view</strong> - consent is given, page not cached</li>
<li><strong>Opt-in after page load</strong> - measure just the opt-in activity</li>
<li><strong>Opted-in repeat view</strong> - cached experience, with cookies</li>
</ul>
<p>The results can be... eye opening. In the following sections we will walk through how to succesfully test these states, how to compare the results and present a small case study based on a corporate website.</p>
<h2 id="opt-in-techniques" tabindex="-1">Opt-in Techniques <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#opt-in-techniques" aria-hidden="true">#</a></h2>
<p>There are generally two methods to test the opt-in process in synthetic tests: setting the opt-in cookie manually or effectively clicking on the "accept" or "I agree" elements. I'll show you how to do both using WebPageTest scripts, this is one of the many surprisingly powerful features of WPT and I recommend reading up on <a href="https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/scripting">the documentation</a>. To learn even more about WPT, I recommend you grab a copy of the book <a href="https://www.amazon.com/_/dp/1491902590?tag=webnin-21">Using WebPageTest</a></p>
<p>The Script UI is shown below for reference.</p>
<figure class="no-shadow clickable"><a href="https://simonhearne.com/images/wpt-cookies/wpt1.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/n8szM_CYic-600.avif 600w, https://simonhearne.com/img/n8szM_CYic-900.avif 900w, https://simonhearne.com/img/n8szM_CYic-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/n8szM_CYic-600.webp 600w, https://simonhearne.com/img/n8szM_CYic-900.webp 900w, https://simonhearne.com/img/n8szM_CYic-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/n8szM_CYic-600.jpeg 600w, https://simonhearne.com/img/n8szM_CYic-900.jpeg 900w, https://simonhearne.com/img/n8szM_CYic-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="screenshot of adding a custom script to web page test" loading="lazy" decoding="async" src="https://simonhearne.com/img/n8szM_CYic-600.jpeg" width="1200" height="908" /></picture></a><figcaption>Adding a script to WebPageTest</figcaption></figure>
<h3 id="setting-an-opt-in-cookie" tabindex="-1">Setting an opt-in cookie <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#setting-an-opt-in-cookie" aria-hidden="true">#</a></h3>
<p>This can actually be quite tricky and requires some detective work. Consent is <em>normally</em> stored in a cookie, which means we can simulate the opt-in state by setting that cookie value manually. First, though, we need to find out what cookie to set!</p>
<p>Open a new private / incognito tab and crack open the developer tools on your browser of choice. Head to the section which shows cookies (<code>Application</code> in Chrome) and load the page under test.</p>
<p>Observe the cookies that are set on the initial page load (and screenshot them if you wish). Then accept the cookie banner and watch what changes! Unfortunately, there are likely to be a lot of new cookies, so spotting the one you care about can be difficult. Here, for example, is an enticingly named <code>CONSENT</code> cookie, but it is not the cookie we are looking for. We can tell this as the cookie domain is <code>google.com</code>, not that of the page under test:</p>
<figure class="no-shadow clickable"><a href="https://simonhearne.com/images/wpt-cookies/cookies.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/ztVsTBQS0N-600.avif 600w, https://simonhearne.com/img/ztVsTBQS0N-900.avif 900w, https://simonhearne.com/img/ztVsTBQS0N-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ztVsTBQS0N-600.webp 600w, https://simonhearne.com/img/ztVsTBQS0N-900.webp 900w, https://simonhearne.com/img/ztVsTBQS0N-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ztVsTBQS0N-600.jpeg 600w, https://simonhearne.com/img/ztVsTBQS0N-900.jpeg 900w, https://simonhearne.com/img/ztVsTBQS0N-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Right-click context menu in developer tools showing copy selector option" loading="lazy" decoding="async" src="https://simonhearne.com/img/ztVsTBQS0N-600.jpeg" width="1200" height="748" /></picture></a><figcaption>Right-click context menu in developer tools showing copy selector option</figcaption></figure>
<p>You can also try filtering the cookies by the term <code>consent</code> to see if anything jumps out, and if all else fails? View-source!</p>
<p>If you view-source and search for "cookie" or "consent", you are likely to find the script responsible for the cookie banner. It might look something like this:</p>
<pre class="language-html"><code class="language-html"><span class="token comment"><!-- OneTrust Cookies Consent Notice start --></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.cookielaw.org/scripttemplates/otSDKStub.js<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/javascript<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text/javascript<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br /><span class="token keyword">function</span> <span class="token function">OptanonWrapper</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>window<span class="token punctuation">.</span>dataLayer<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token literal-property property">event</span><span class="token operator">:</span><span class="token string">'OneTrustGroupsUpdated'</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><br /></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span><br /><span class="token comment"><!-- OneTrust Cookies Consent Notice end --></span></code></pre>
<p>This in turn points me at <a href="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js">https://cdn.cookielaw.org/scripttemplates/otSDKStub.js</a>. Loading that script shows me right at the top of the file: <code>this.optanonCookieName="OptanonConsent"</code>. Sleuthing complete! Back in developer tools we can filter cookies to <code>OptanonConsent</code> and see if any have been set.</p>
<figure class="no-shadow clickable"><a href="https://simonhearne.com/images/wpt-cookies/optanon.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/ucdwPQaaCe-600.avif 600w, https://simonhearne.com/img/ucdwPQaaCe-900.avif 900w, https://simonhearne.com/img/ucdwPQaaCe-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ucdwPQaaCe-600.webp 600w, https://simonhearne.com/img/ucdwPQaaCe-900.webp 900w, https://simonhearne.com/img/ucdwPQaaCe-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ucdwPQaaCe-600.jpeg 600w, https://simonhearne.com/img/ucdwPQaaCe-900.jpeg 900w, https://simonhearne.com/img/ucdwPQaaCe-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Developer tools showing optnanon cookie is set" loading="lazy" decoding="async" src="https://simonhearne.com/img/ucdwPQaaCe-600.jpeg" width="1200" height="407" /></picture></a><figcaption>Developer tools showing optnanon cookie is set</figcaption></figure>
<p>Once you have the cookie, copy the value that has been set. It may be something simple like <code>true</code> or <code>1</code>, or it could be more obtuse and include a unique ID, timestamp and any number of other variables. We can add the cookie to our test scripts using a WebPageTest script and the <code>setCookie</code> command. Note that the domain should have <code>http://</code> as the protocol and should <em>not</em> have a trailing slash. Double check this if you see results like <code>(Test Error: Unhandled exception in test run: coercing to Unicode: need string or buffer, int found)</code></p>
<pre class="language-text"><code class="language-text">setCookie http://www.akamai.com OptanonConsent=consentId=1f6c1653-871e-468d-913c-378d05a8539e&datestamp=Tue+May+12+2020+21%3A42%3A07+GMT%2B0100+(British+Summer+Time)&version=5.15.0&interactionCount=2&isIABGlobal=false&landingPath=NotLandingPage&groups=&hosts=&legInt=<br />setCookie http://www.akamai.com OptanonAlertBoxClosed=2020-05-13T06:20:21.745Z<br />navigate %URL%</code></pre>
<p>If the cookie value has a timestamp it may become invalid after a while, so if you are running these tests in an automated environment (e.g. CI testing or monitoring) then you may need to make this dynamic at the time of the test.</p>
<p>You may also notice that I've set two cookies in the script above. The first is the actual consent, allowing Google Tag Manager to inject third-parties. The second is necessary to prevent the consent banner re-appearing. Why are they separate? Who knows.</p>
<h3 id="clicking-on-the-element" tabindex="-1">Clicking on the Element <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#clicking-on-the-element" aria-hidden="true">#</a></h3>
<p>This is more simple, conceptually, than setting the cookie in a script. Here we emulate a user actively opting in by triggering the event in the website's code. To do this in WPT, we just need a valid selector for the accept action. Right-click on the element in your browser of choice and hit "Inspect".</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/inspect-elem.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/w6OSVjtc_q-600.avif 600w, https://simonhearne.com/img/w6OSVjtc_q-900.avif 900w, https://simonhearne.com/img/w6OSVjtc_q-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/w6OSVjtc_q-600.webp 600w, https://simonhearne.com/img/w6OSVjtc_q-900.webp 900w, https://simonhearne.com/img/w6OSVjtc_q-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/w6OSVjtc_q-600.jpeg 600w, https://simonhearne.com/img/w6OSVjtc_q-900.jpeg 900w, https://simonhearne.com/img/w6OSVjtc_q-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Right-click context menu in browser showing inspect element option" loading="lazy" decoding="async" src="https://simonhearne.com/img/w6OSVjtc_q-600.jpeg" width="1200" height="221" /></picture></a><figcaption>Right-click context menu in browser showing inspect element option</figcaption></figure>
<p>Once you have the element highlighted in the developer tools window, you can quickly observe if there is an easy selector such as an element ID (present in this example). If not, you can right-click on the element here and copy a selector. This can often end up being pretty long if the element does not have an ID, but it should at least give you a good starting point:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/copy-selector.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/68moKyCAyM-600.avif 600w, https://simonhearne.com/img/68moKyCAyM-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/68moKyCAyM-600.webp 600w, https://simonhearne.com/img/68moKyCAyM-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/68moKyCAyM-600.jpeg 600w, https://simonhearne.com/img/68moKyCAyM-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Right-click context menu in developer tools showing copy selector option" loading="lazy" decoding="async" src="https://simonhearne.com/img/68moKyCAyM-600.jpeg" width="900" height="406" /></picture></a><figcaption>Right-click context menu in developer tools showing copy selector option</figcaption></figure>
<p>Once you have your selector, check it works by heading to the browser console and attempting to select it:</p>
<pre class="language-js"><code class="language-js">document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'#onetrust-accept-btn-handler'</span><span class="token punctuation">)</span></code></pre>
<p>If the result is the element then you are on to a winner! We now just need to add this to our WPT script. There are a number of ways to do this, including WPT functions like <code>click</code> and <code>clickAndWait</code>. I find that this can get frustrating at times when switching between WPT and the browser console, so I tend do use <code>exec</code> or <code>execAndWait</code> which just execute the JavaScript you provide. If you need to chain multiple actions such as opening a modal then clicking a button, make sure your last action is <code>execAndWait</code> and preceding actions are <code>exec</code>. Assuming a simple interation, we will want a script that looks like:</p>
<pre class="language-text"><code class="language-text">setEventName loadPage<br />navigate %URL%<br />setEventName acceptCookies<br />execAndWait document.QuerySelector('#onetrust-accept-btn-handler').click()</code></pre>
<p>This will load the page at the URL you have entered in the URL field as the first step, then accept cookies and record any further activity as a separate step / page load. If you'd like to combine these you can use <code>combineSteps</code>, although note that there may be a small gap in the waterfall between the page loading and the script executing:</p>
<pre class="language-text"><code class="language-text">combineSteps<br />navigate %URL%<br />execAndWait document.QuerySelector('#onetrust-accept-btn-handler').click()</code></pre>
<p>In this case, the button will be clicked shortly after the initial page load, close to the expected user behaviour. Both test options are valid, and you should try both!</p>
<h3 id="bonus-tips" tabindex="-1">Bonus Tips <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#bonus-tips" aria-hidden="true">#</a></h3>
<p>Here are a few handy tips for working with WPT and cookie consent prompts.</p>
<h4 id="clearing-up-cookies" tabindex="-1">Clearing up cookies <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#clearing-up-cookies" aria-hidden="true">#</a></h4>
<p>It is quite likely that you will need to emulate a first-time viewer multiple times in this process. The most simple method to reset state is to clear site data. In Chrome, this is under the "Applications" tab. Once cleared, hit Cmd+R / Ctrl+F5 to perform a hard reload.</p>
<figure class:"no-shadow"="">
<img src="https://simonhearne.com/images/wpt-cookies/clear-data.png" alt="Screenshot of developer tools showing clear site data option" loading="lazy" clickable="true" />
</figure>
<h4 id="debugging-wpt-scripts" tabindex="-1">Debugging WPT Scripts <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#debugging-wpt-scripts" aria-hidden="true">#</a></h4>
<p>WPT scripts do not provide debugging information, but we can debug the output. In this case we are expecting to see either a cookie being sent with the page request, or an element being clicked in the page load. Use the filmstrip view or page screenshot to check that the page looks as you expect for an opt-in user, i.e. the cookie banner is not present. If the banner is not present, you need to verify that your JavaScript actually works in your browser or check that the cookie value is being set.</p>
<p>To check the cookie value, click on the test waterfall to get to the Details view, then click on the first request to open the modal request viewer. Click on the request tab and check that your cookie header is being set:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/wpt-cookie.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/ln6rZAZpH9-600.avif 600w, https://simonhearne.com/img/ln6rZAZpH9-900.avif 900w, https://simonhearne.com/img/ln6rZAZpH9-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ln6rZAZpH9-600.webp 600w, https://simonhearne.com/img/ln6rZAZpH9-900.webp 900w, https://simonhearne.com/img/ln6rZAZpH9-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ln6rZAZpH9-600.jpeg 600w, https://simonhearne.com/img/ln6rZAZpH9-900.jpeg 900w, https://simonhearne.com/img/ln6rZAZpH9-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Screenshot of web page test showing cookie data in request header" loading="lazy" decoding="async" src="https://simonhearne.com/img/ln6rZAZpH9-600.jpeg" width="1200" height="638" /></picture></a><figcaption>Screenshot of web page test showing cookie data in request header</figcaption></figure>
<p>If not, you might need to try experimenting with syntax until it works 🙂</p>
<h4 id="consent-button-in-iframes" tabindex="-1">Consent Button in iframes <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#consent-button-in-iframes" aria-hidden="true">#</a></h4>
<p>Some cookie consent services may load the banner in an iframe, this makes selecting content a little more tricky. First you need to select the iframe document and then select the button / confirmation element within it:</p>
<pre class="language-text"><code class="language-text">execAndWait document.querySelector('iframe[name=consentFrame]').contentWindow.document.querySelector('#confirmBtn').click()</code></pre>
<h4 id="geographic-variations" tabindex="-1">Geographic Variations <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#geographic-variations" aria-hidden="true">#</a></h4>
<p>Some sites may have different policies based on the location of the visitor, for example respecting CCPA in California vs. Cookie Law in EU. I would recommend testing from at least one North American location and one European location. Countries within Europe may have slightly different laws, so testing from the Netherlands (known as relatively strict) is a good choice.</p>
<h2 id="results" tabindex="-1">Results <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#results" aria-hidden="true">#</a></h2>
<p>We need a test subject to use as an example for the following sections. I wanted to use a website which implements cookie consent, but is not heavily driven by third-party content (i.e. not a digital publisher). Corporate websites are great for this, so that's what we'll look at.</p>
<p>The headlines are:</p>
<ul>
<li>Opted out experiences are ~35% faster</li>
<li>Opting in downloads 2.5MB of additional JavaScript</li>
<li>Opted in repeat views are twice as slow as opted out</li>
</ul>
<p>The following sections go into a bit more detail about each test. Below are links to the test results:</p>
<ul>
<li><a href="https://www.webpagetest.org/result/200512_86_c05f465592d3345f7563427f9ec0b2c3/">Opted-out first view</a></li>
<li><a href="https://www.webpagetest.org/result/200513_G2_e2b7bbe825a7d280cecce020b83d0ff4/">Opted-in first view</a></li>
<li><a href="https://www.webpagetest.org/result/200512_B0_60886c4f172e0e4bc39db656a9217872/">Opt-in after page load</a></li>
</ul>
<h3 id="first-view-comparison" tabindex="-1">First View Comparison <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#first-view-comparison" aria-hidden="true">#</a></h3>
<p>This is the most common type of comparison you are likely to perform, what does a full page load look like with and without consent? I've used the handy <a href="https://wpt-compare.app/">wpt-compare.app</a> by <a href="https://twitter.com/therealnooshu">Matt Hobbs</a> to make this a little easier 🙂</p>
<figure class="scroller no-shadow">
<img src="https://simonhearne.com/images/wpt-cookies/filmstrip1_250.png" alt="Filmstrip images showing cookie opt-in is slower" loading="lazy" srcset="https://simonhearne.com/images/wpt-cookies/filmstrip1_125.png 1x, https://simonhearne.com/images/wpt-cookies/filmstrip1_250.png 2x, https://simonhearne.com/images/wpt-cookies/filmstrip1_375.png 3x" />
</figure>
<figcaption class="scroller">Filmstrip comparison showing similar visual performance. <a href="https://www.webpagetest.org/video/compare.php?tests=200512_86_c05f465592d3345f7563427f9ec0b2c3-l%3AOpted+out-r%3A5%2C200513_G2_e2b7bbe825a7d280cecce020b83d0ff4-l%3AOpted+in-r%3A4&thumbSize=200&ival=500&end=visual&bg=fff&text=000">Source</a></figcaption>
<p>The visual performance comparison shows relatively little difference between the initial render performance as we would expect, except for the visual addition of the cookie banner. The total load time was somewhat higher for the opted-in test, at 5s vs 4.2s for opted-out.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/timings-first-view.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/xodKeCEiq8-600.avif 600w, https://simonhearne.com/img/xodKeCEiq8-900.avif 900w, https://simonhearne.com/img/xodKeCEiq8-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/xodKeCEiq8-600.webp 600w, https://simonhearne.com/img/xodKeCEiq8-900.webp 900w, https://simonhearne.com/img/xodKeCEiq8-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/xodKeCEiq8-600.jpeg 600w, https://simonhearne.com/img/xodKeCEiq8-900.jpeg 900w, https://simonhearne.com/img/xodKeCEiq8-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrip images showing cookie opt-in is slower" loading="lazy" decoding="async" src="https://simonhearne.com/img/xodKeCEiq8-600.jpeg" width="1200" height="635" /></picture></a><figcaption>Timing comparison showing significant increases for opt-in on first view. <a href="https://www.webpagetest.org/video/compare.php?tests=200512_86_c05f465592d3345f7563427f9ec0b2c3-l%3AOpted+out-r%3A5%2C200513_G2_e2b7bbe825a7d280cecce020b83d0ff4-l%3AOpted+in-r%3A4&thumbSize=200&ival=500&end=visual&bg=fff&text=000">Source</a></figcaption></figure>
<p>Looking at more of the timers we can see some significant degradations. Notably, CPU Busy Time increases 2.5x from 4.9s to 12s, indicating that the third-party content has a significant impact on the browser main thread. This could cause a laggy experience, especially on mobile devices. The visual performance of the page is impacted as well, with the visual complete time increasing by 35% from 5.4s to 7.3s.</p>
<p>The request maps below show the significance of the opt-in on the diversity of domains on the webpage. The number of requests increases 2.4x from 60 to 144, with an additional 30 TCP connections established.</p>
<div class="two-fig-cols"><figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/reqmap-1a.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/QMMcLngM-M-600.avif 600w, https://simonhearne.com/img/QMMcLngM-M-900.avif 900w, https://simonhearne.com/img/QMMcLngM-M-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/QMMcLngM-M-600.webp 600w, https://simonhearne.com/img/QMMcLngM-M-900.webp 900w, https://simonhearne.com/img/QMMcLngM-M-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/QMMcLngM-M-600.jpeg 600w, https://simonhearne.com/img/QMMcLngM-M-900.jpeg 900w, https://simonhearne.com/img/QMMcLngM-M-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Request map of opted-out experience showing few domains" loading="lazy" decoding="async" src="https://simonhearne.com/img/QMMcLngM-M-600.jpeg" width="1200" height="842" /></picture></a><figcaption>Requestmap for opted-out experience. <a href="https://requestmap.webperf.tools/render/200512_86_c05f465592d3345f7563427f9ec0b2c3/">Source</a></figcaption></figure><figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/reqmap-2a.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/eMoqZ9xIxy-600.avif 600w, https://simonhearne.com/img/eMoqZ9xIxy-900.avif 900w, https://simonhearne.com/img/eMoqZ9xIxy-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/eMoqZ9xIxy-600.webp 600w, https://simonhearne.com/img/eMoqZ9xIxy-900.webp 900w, https://simonhearne.com/img/eMoqZ9xIxy-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/eMoqZ9xIxy-600.jpeg 600w, https://simonhearne.com/img/eMoqZ9xIxy-900.jpeg 900w, https://simonhearne.com/img/eMoqZ9xIxy-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Request map of opted-out experience showing few domains" loading="lazy" decoding="async" src="https://simonhearne.com/img/eMoqZ9xIxy-600.jpeg" width="1200" height="842" /></picture></a><figcaption>Requestmap for opted-in experience. <a href="https://requestmap.webperf.tools/render/200513_G2_e2b7bbe825a7d280cecce020b83d0ff4/">Source</a></figcaption></figure>
</div>
<h3 id="repeat-view-comparison" tabindex="-1">Repeat View Comparison <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#repeat-view-comparison" aria-hidden="true">#</a></h3>
<p>Whereas first-party content is likely to be cached for a repeat view, third-party content may be downloaded again. Third-party trackers and analytics scripts will also execute on every page view, potentially increasing the relative impact of these on cached experiences.</p>
<figure class="scroller no-shadow">
<img src="https://simonhearne.com/images/wpt-cookies/filmstrip2_250.png" alt="Filmstrip images showing cookie opt-in is slower" loading="lazy" srcset="https://simonhearne.com/images/wpt-cookies/filmstrip2_125.png 1x, https://simonhearne.com/images/wpt-cookies/filmstrip2_250.png 2x, https://simonhearne.com/images/wpt-cookies/filmstrip2_375.png 3x" />
</figure>
<figcaption class="scroller">Filmstrip comparison showing opt-in performance is slower. <a href="https://www.webpagetest.org/video/compare.php?tests=200512_86_c05f465592d3345f7563427f9ec0b2c3-l%3AOpted+out-r%3A3-c%3A1%2C200513_G2_e2b7bbe825a7d280cecce020b83d0ff4-l%3AOpted+in-r%3A3-c%3A1&thumbSize=200&ival=200&end=visual&bg=fff&text=000">Source</a></figcaption>
<p>The visual performance comparison above shows a close race, with the opted-out experience winning by a small margin of 200ms. Note that the opted-in experience filmstrip continues for another 2.5s. Again, the timers tell a more interesting story:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/timings-repeat-view.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/fLGZmsbEBj-600.avif 600w, https://simonhearne.com/img/fLGZmsbEBj-900.avif 900w, https://simonhearne.com/img/fLGZmsbEBj-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/fLGZmsbEBj-600.webp 600w, https://simonhearne.com/img/fLGZmsbEBj-900.webp 900w, https://simonhearne.com/img/fLGZmsbEBj-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/fLGZmsbEBj-600.jpeg 600w, https://simonhearne.com/img/fLGZmsbEBj-900.jpeg 900w, https://simonhearne.com/img/fLGZmsbEBj-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrip images showing cookie opt-in is slower" loading="lazy" decoding="async" src="https://simonhearne.com/img/fLGZmsbEBj-600.jpeg" width="1200" height="636" /></picture></a><figcaption>Timing comparison showing significant increases for opt-in on repeat view. <a href="https://www.webpagetest.org/video/compare.php?tests=200512_86_c05f465592d3345f7563427f9ec0b2c3-l%3AOpted+out-r%3A3-c%3A1%2C200513_G2_e2b7bbe825a7d280cecce020b83d0ff4-l%3AOpted+in-r%3A3-c%3A1&thumbSize=200&ival=200&end=visual&bg=fff&text=000">Source</a></figcaption></figure>
<p>CPU Busy Time again shows a significant jump, as do the visual metrics. These show that the relative impact of the opted-in experience is much greater on cached views. Page load increases over 160% from 1.2s to 3.2s and visual complete more than doubles from 1.9s to 4.9s!</p>
<h3 id="opt-in-after-page-load" tabindex="-1">Opt-in after page load <a class="direct-link" href="https://simonhearne.com/2020/testing-behind-consent/#opt-in-after-page-load" aria-hidden="true">#</a></h3>
<p>The sections above have shown the impact of cookie consent on page load performance, but what about the actual action of consenting? Clicking the button immediately triggers a cascade of requests from third-parties, each hoping to collect as much data as possible. We can measure this performance impact by splitting the navigate and action steps, then we can analyse just the activity which occurs after the opt-in event.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/wpt-cookies/optin-breakdown.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/iXF7zC5eoz-600.avif 600w, https://simonhearne.com/img/iXF7zC5eoz-900.avif 900w, https://simonhearne.com/img/iXF7zC5eoz-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/iXF7zC5eoz-600.webp 600w, https://simonhearne.com/img/iXF7zC5eoz-900.webp 900w, https://simonhearne.com/img/iXF7zC5eoz-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/iXF7zC5eoz-600.jpeg 600w, https://simonhearne.com/img/iXF7zC5eoz-900.jpeg 900w, https://simonhearne.com/img/iXF7zC5eoz-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrip images showing cookie opt-in is slower" loading="lazy" decoding="async" src="https://simonhearne.com/img/iXF7zC5eoz-600.jpeg" width="1200" height="709" /></picture></a><figcaption>Content breakdown for the opt-in interaction. <a href="https://www.webpagetest.org/result/200512_B0_60886c4f172e0e4bc39db656a9217872/1/breakdown/">Source</a></figcaption></figure>
<p>Almost 2.5MB of JavaScript content is loaded across 33 objects, at a cost of 680kB of bandwidth and 1.8s of download time. Knowing this, would you opt-in to cookies? What's the performance cost for your opt-in experience?</p>
<p>As for the UX of cookie consent, Vitaly Friedman has <a href="https://www.smashingmagazine.com/2019/04/privacy-ux-better-cookie-consent-experiences/">written about</a> the varied (and extensive) issues with the current state of cookie prompts. Vitaly covers this and more in his excellent <a href="https://www.youtube.com/watch?v=Jz3Fu1o356g">closing keynote</a> at performance.now() conference 2019.</p>
When Network is Faster than Cache2020-05-07T00:00:00Zhttps://simonhearne.com/2020/network-faster-than-cache/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#introduction" aria-hidden="true">#</a></h2>
<p>I recently discovered that Firefox introduced a feature called <code>RCWN</code> (Race Cache With Network) <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1392841">in 2017</a>. This feature was intended to improve web performance by racing cached requests against the network.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/OubkAj4BVh-204.avif 204w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/OubkAj4BVh-204.webp 204w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Firefox network panel showing cached resources 'raced' againt the network" loading="lazy" decoding="async" src="https://simonhearne.com/img/OubkAj4BVh-204.jpeg" width="204" height="192" /></picture><figcaption>Fig 1. Firefox network panel showing cached resources 'raced' againt the network</figcaption></figure>
<p>This seemed odd to me, surely using a cached response would always be faster than making the whole request again! Well it turns out that in some cases, the network is faster than the cache. There is some <a href="https://groups.google.com/a/chromium.org/forum/#!msg/net-dev/DGVo2J4GKzc/fWNljySeAQAJ">discussion on mailing lists way back in 2016</a> about the performance differences between browsers. "Firefox cache is performing better than Chrome on windows. Almost 25% fewer >1 second loads" reads one forum. More recently, someone mentioned on the <a href="https://webperformance.herokuapp.com/">Web Performance Slack</a> that they had seen their cache take over three seconds to retrieve an object! This led me down a bit of a rabbit hole investigating cached responses:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/JJmZu89hEy-600.avif 600w, https://simonhearne.com/img/JJmZu89hEy-900.avif 900w, https://simonhearne.com/img/JJmZu89hEy-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/JJmZu89hEy-600.webp 600w, https://simonhearne.com/img/JJmZu89hEy-900.webp 900w, https://simonhearne.com/img/JJmZu89hEy-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/JJmZu89hEy-600.jpeg 600w, https://simonhearne.com/img/JJmZu89hEy-900.jpeg 900w, https://simonhearne.com/img/JJmZu89hEy-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Chrome network panel showing resources served from cache taking 3.4 seconds" loading="lazy" decoding="async" src="https://simonhearne.com/img/JJmZu89hEy-600.jpeg" width="1200" height="350" /></picture><figcaption>Fig 2. A resource served from cache taking >3,400ms (Chrome v77 on SSD Macbook Pro 2015)</figcaption></figure>
<p>The data in the mailing groups was relatively sparse and tended to focus on a specific browser, so I wondered if I could use <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">mPulse</a> to build a bigger picture. mPulse collects resource timing data from every visit to a client's website using the Resource Timing API. It is possible to determine if a resource was served from cache using a simple filter when querying the data: <code>bool resourceFromCache = (transferredSize == 0 && encodedSize > 0)</code>. This filter sets a boolean of whether the resource was fetched from cache, based on the resource having a file size, but no network download. This allows us to group aggregated resource timing data by whether it was served from cache or network.</p>
<p>Responses to conditional GET requests (if-not-modified and if-modified-since) will be excluded from this data, as the transferred size is greater than zero due to the presence of response headers.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/6Ha8Xcneyu-600.avif 600w, https://simonhearne.com/img/6Ha8Xcneyu-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/6Ha8Xcneyu-600.webp 600w, https://simonhearne.com/img/6Ha8Xcneyu-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/6Ha8Xcneyu-600.jpeg 600w, https://simonhearne.com/img/6Ha8Xcneyu-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Chrome console showing resource timing entry for a cached resource" loading="lazy" decoding="async" src="https://simonhearne.com/img/6Ha8Xcneyu-600.jpeg" width="900" height="586" /></picture><figcaption>Fig 3. Resource timing entry for resource in Fig 1., showing the timing points available to JavaScript</figcaption></figure>
<p>So why would a cached response take so long to fetch? Well the answer, frustratingly, is a bit of a shrug. There have been bugs in Chromium with request prioritisation, where cached resources were delayed while the browser fetched higher priority requests over the network. There is also the obvious cost of fetching the resource from memory. It is fair to assume that the memory cache at a CDN or ISP proxy server is higher performance than the average mobile device, and the cost of an additional request over an H/2 connection on a good connection is negligible. For desktop devices, reading from disk may have a significant cost compared to a multi-megabit wired network connection. This comment from one of the firefox bugs raised on <code>RCWN</code> shows a desktop user with 1.3% successful network race. While the bug is complaining that the races are too frequent, I'm surprised that they win at all!</p>
<blockquote>
<p>On my OSX box I'm seeing us race more than we probably need to:</p>
<p>Total network request count: 5574
Cache won count 938
Net won count 13</p>
<p>That's racing almost 16% of the time, but only winning 1.3% of the time. We should probably back off on racing a bit in this case, at least.
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1425268">Tune RCWN racing parameters (and make them pref-able)</a></p>
</blockquote>
<p>This strange phenomenon will also mean that you could see your browser making fetches for cached immutable assets! This was recently noticed by <a href="https://twitter.com/ericlaw">Eric Lawrence</a> of <a href="https://www.telerik.com/fiddler">Fiddler</a> fame:</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">Firefox 78 periodically refetching "immutable" resources: <a href="https://t.co/0UtRiSSpvf">pic.twitter.com/0UtRiSSpvf</a></p>— Eric Lawrence 🎻 (@ericlaw) <a href="https://twitter.com/ericlaw/status/1257469057319817218?ref_src=twsrc%5Etfw">May 5, 2020</a></blockquote>
<p>This all led me to dig into some data at scale and see if there are any interesting patterns we can use, and what we can do about it! I'll be looking at <code>cache retrieval</code> time, which I define as the time between request start and response end, for a resource served from cache.</p>
<p>The results show that cache retrieval performance is impacted most by two factors:</p>
<ul>
<li>Device hardware</li>
<li>Total number of assets served from cache</li>
</ul>
<p>Read on to learn about the methodology for collecting the data, or skip straight to <a href="https://simonhearne.com/2020/network-faster-than-cache/#results">the results</a>.</p>
<h2 id="data-collection" tabindex="-1">Data Collection <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#data-collection" aria-hidden="true">#</a></h2>
<p>All data used in this analysis is collected using the open source library <a href="https://github.com/akamai/boomerang">Boomerang</a>. Boomerang is the jQuery of performance measurement in the browser, smoothing over the cracks in browser APIs to provide a consistent source of performance data back to ancient versions of IE. To take a look at the data available in your browser run the following in your developer console:</p>
<pre class="language-js"><code class="language-js">window<span class="token punctuation">.</span>performance<span class="token punctuation">.</span>getEntriesByType<span class="token punctuation">[</span><span class="token string">'resource'</span><span class="token punctuation">]</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span></code></pre>
<p>This will expose the resource timing data for the first object in the current page (excluding the HTML document - that's in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API">Navigation Timing API</a> data).</p>
<p>Boomerang will then compress this data using a <a href="https://nicj.net/compressing-resourcetiming/">clever trie algorithm</a> and send it to a collector, for every pageview. mPulse is an enterprise solution to manage this process, collect and store the data and provide dashboards and alerts. Resource Timing data is stored for 14 days, so I have two weeks of data to analyse at any time. Along with the timing information, mPulse stores a large amount of data about the device which made the request - including manufacturer, available memory, battery state, network state and more! All of this depends on the data being available to JavaScript, so it is not guaranteed across platforms.</p>
<h3 id="limitations" tabindex="-1">Limitations <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#limitations" aria-hidden="true">#</a></h3>
<p>There are a number of limitations to data collected from the browser (field data).</p>
<ul>
<li><strong>Timing-Allow-Origin</strong> - resource timing information is not available for cross-origin domains, unless the host opts in by setting a <code>timing-allow-origin</code> header. This means that we lose data for approximately 70% of third-party requests. These are rarely cacheable, and it is not a problem that we can fix, so it is ignored.</li>
<li><strong>Resource Timing Support</strong> - RT is <a href="https://caniuse.com/#feat=resource-timing">reasonably well supported</a> at the time of writing, but we do miss Opera browser entirely.</li>
<li><strong>Late Loaded Resources</strong> - Boomerang collects data for resources which occur before the beacon is fired, this is typically at window loaded. As such we do not have data for resources loaded later in the page lifecycle.</li>
<li><strong>Size of Data</strong> - There could easily be billions of data points for a query across all resource timing data, I tended to narrow down my queries to a minimum of a 24hr time window and use basic data hygeine to exclude bad results.</li>
<li><strong>Non-direct Comparison</strong> - It is not trivial to compare directly a cache miss and a cache hit in large-scale RUM data, as this would require a comparison at almost the individual device level. We are using aggregates here to look at general performance rather than a direct comparison of cache vs. network.</li>
</ul>
<h2 id="results" tabindex="-1">Results <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#results" aria-hidden="true">#</a></h2>
<p>I wanted to find out if there were any common dimensions which will lead to a slow cache retrieval time. We saw earlier that Firefox might be faster than Chrome, but what about small vs. large resources, scripts vs. images, mobile vs. desktop?</p>
<p>You could simply compare statistics on this data, but where's the fun in that? You will find a set of distributions in the following sections, each distribution shows the performance of cache retrieval by the given dimension. Sharp peaks to the left indicate fast and consistent retrieval times, broad and low charts indicate slow and variable retrieval times.</p>
<p>The mailing lists (and the complaint on Slack) showed retrieval times that took multiple seconds. In the data I see this happen extremely infrequently, so instead I focus on retrievals in the time range ≤250ms to spot differences across the dimensions. You can change the scale of both axes using one of the sliders below the charts. There is also a checkbox to show cache miss results (i.e. requests that go to the network), we can expect these to normally be quite slow so the relative height of the chart will be small.</p>
<h3 id="by-resource-size" tabindex="-1">By Resource Size <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#by-resource-size" aria-hidden="true">#</a></h3>
<p>My first hypothesis was that there might be a correlation between resource size and cache retrieval duration. It makes sense that it would take longer to retrieve an 800kB image from cache than a 10kB one. Well, it turns out that cache performance is pretty consistent across resource sizes. Once you get to 100kB - 1MB the retrieval times take a slight (<1ms) dip, but overall not a huge shift.</p>
<div class="vega-chart loading" id="vis1" data-spec="/data/sizecachehists.json">Loading chart...</div>
<h3 id="by-resource-type" tabindex="-1">By Resource Type <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#by-resource-type" aria-hidden="true">#</a></h3>
<p>My second hypothesis was that browsers might treat different resources differently, a cached CSS asset should be a higher priority than a cached image one might assume. Again, there is no real correlation here.</p>
<div class="vega-chart loading" id="vis2" data-spec="/data/assettypehists.json">Loading chart...</div>
<h3 id="by-browser" tabindex="-1">By Browser <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#by-browser" aria-hidden="true">#</a></h3>
<p>Now we're getting to the good stuff, do different browsers perform differently? My hypothesis was that we would see a difference between browsers used on mobile and desktop devices. We see that this is true, for some browsers!</p>
<p>Desktop Chrome has a much more consistent cache retrieval time than Chrome Mobile, with Samsung Internet showing a really broad distribution. On the other hand, Mobile Safari appears to perform better than desktop Safari - indicative of Apple's continued investment into mobile hardware.</p>
<div class="vega-chart loading" id="vis3" data-spec="/data/browsercachehists.json">Loading chart...</div>
<h3 id="by-operating-system" tabindex="-1">By Operating System <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#by-operating-system" aria-hidden="true">#</a></h3>
<p>Ok but Browsers are installed across lots of different devices, what if we broke this down to Operating Systems? Here we're starting to get some interesting data. Desktop Operating Systems perform better than mobile, and Android performs worse than iOS. This makes sense, Apple controls the hardware on which iOS runs, whereas Android (through the Android Open Source Project) could be running on anything from a $25 feature phone right up to a $1,000+ flagship.</p>
<div class="vega-chart loading" id="vis4" data-spec="/data/osnamehists.json">Loading chart...</div>
<h3 id="by-manufacturer" tabindex="-1">By Manufacturer <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#by-manufacturer" aria-hidden="true">#</a></h3>
<p>Let's dig down a bit further and look at the performance for actual devices. Here we see a bubble plot including the device type, operating system and reported device manufacturer. You will notice that the vast majority of bubbles are Android - this is due to a combination of two factors:</p>
<ul>
<li>Desktop devices generally do not advertise manufacturer in the User Agent String</li>
<li>Android has, by far, the greatest manufacturer diversity of all Operating Systems</li>
</ul>
<p>I've highlighted on the chart those devices which have an median cache retrieval time of >100ms. This is <strong>per resource</strong>, user experiences on these devices are likely to suck. On the other end of the spectrum, high-end manufacturers such as RED, Razer & Apple demonstrate consistently fast retrieval times, with a median <8ms.</p>
<div class="vega-chart loading" id="vis5" data-spec="/data/cachetimings.json">Loading chart...</div>
<h3 id="a-bit-deeper" tabindex="-1">A Bit Deeper <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#a-bit-deeper" aria-hidden="true">#</a></h3>
<p>I decided to make some focussed queries in order to get a more definitive picture of cache retrieval performance. The most obvious thing to do is track down a single cacheable asset and focus on that. A quick look at the data shows the most common resource is, unsurprisingly, Google Analytics: <code>analytics.js</code>.</p>
<p>Unfortunately the Google Analytics JS is not sent with a <code>timing-allow-origin</code> header. As such, we cannot tell if it was delivered from cache or not 😔 I modified my search to only include resources which were both cacheable and had the <code>timing-allow-origin</code> header. The top result was again from Google, this time their ad tracking script: <code>conversion_async.js</code>.</p>
<p>This chart shows the retrieval times for <code>conversion_async.js</code> by manufacturer. Try reducing the y-scale to see the cache misses appear on the charts, note that the long-tail of the cache hits overlaps well into the cache miss performance. This shows how often cache performance is worse than network, and how consistently fast Apple devices are.</p>
<div class="vega-chart loading" id="vis6" data-spec="/data/manufacturerhists.json">Loading chart...</div>
<h2 id="the-obvious-bottleneck" tabindex="-1">The Obvious Bottleneck <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#the-obvious-bottleneck" aria-hidden="true">#</a></h2>
<p>I received a comment from Uwe shortly after publishing this post which got me thinking:</p>
<blockquote class="twitter-tweet" data-conversation="none" data-dnt="true"><p lang="en" dir="ltr">Thank you for this piece of research. <br /><br />If resource size does not correlate much with hit/miss, we are back in HTTP/1 times, where the number of requests is crucial: Concatenate scripts and style sheets as much as possible to avoid requests - not only over the network.</p>— Uwe Trenkner (@utrenkner) <a href="https://twitter.com/utrenkner/status/1258726950388015105?ref_src=twsrc%5Etfw">May 8, 2020</a></blockquote>
<p>Earlier in the post I mentioned that there was a Chromium bug where network requests might block cache requests. Taking that idea a little further: what if our optimisations on the network stack such as HTTP/2 <a href="https://developers.google.com/web/fundamentals/performance/http2#request_and_response_multiplexing">Request Multiplexing</a> have degraded the performance of cache retrieval? File concatenation is generally considered an <a href="https://blog.cloudflare.com/http-2-for-web-developers/#stopconcatenatingfiles">anti-pattern in H/2</a>, but how do device caches handle concurrent reads?</p>
<p>It turns out that there is indeed a strong correlation between the number of resources retrieved from cache and the average retrieval time! This holds especially true for Android & Chrome OS devices. For example: Chrome OS average cache retrieval doubles from ~50ms with five cached resources up to ~100ms with 25 resources.</p>
<div class="vega-chart loading" id="vis7" data-spec="/data/cachesbydevicetype.json">Loading chart...</div>
<p>This data is from a large dataset, but still shows some variance due to the potentially small dimension set sizes such as less popular operating systems. The data shown is only for page loads that had between 1 and 250 assets retrieved from cache. Data points are only included if there were at least 100 occurrences of the combined dimensions (operating system, device type & number of cached resources).</p>
<p>It turns out that Chrome actively throttles requests, including those to cached resources, to reduce I/O contention. This generally improves performance, but will mean that pages with a large number of cached resources will see a slower retrieval time for each resource.</p>
<blockquote>
<p>The short-term proposal is to throttle H2+QUIC requests just as we do for HTTP/1.1 requests today. This ensures that a started request will have to wait for no more than 9 other requests, reducing the TTFB delay for the first request from 400ms to 40ms. Doing so shows an average 25% reduced time-to-first-contentful-paint (TTFCP) for a cached <a href="https://android.com./">https://android.com.</a></p>
</blockquote>
<p class="align-right pull-up small"><a href="https://docs.google.com/document/d/1Aa7OKFRdtmn4IFzgHYfqeqk5lnyTiv4jkwCzMoAlrTU/edit#heading=h.zdlavartcrrw">source</a></p>
<h2 id="so-what" tabindex="-1">So What <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#so-what" aria-hidden="true">#</a></h2>
<p>I was intrigued to dig into this data after reading about browsers racing cache vs. network and I enjoyed pulling data and plotting the charts. But what can we as humble developers actually do with this information?</p>
<h3 id="the-digital-divide-grows-wider" tabindex="-1">The Digital Divide Grows Wider <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#the-digital-divide-grows-wider" aria-hidden="true">#</a></h3>
<p>It turns out that this reinforces a well known but oft-ignored fact: folks on the lower end of the social scale have a worse web browsing experience. We know that cheap devices will have slower processors, less memory and lower bandwidth, and that this leads to a worse perceived page speed. Now we know that even cached experiences are slow for these users. This spreads our concept of a digital speed divide even wider. We also know a little more about browser internals!</p>
<p>The best thing we can do to deliver an inclusive web is to deliver the minimal viable experience, especially to low-end devices. This will benefit all users.</p>
<h3 id="cache-will-not-save-us" tabindex="-1">Cache Will Not Save Us <a class="direct-link" href="https://simonhearne.com/2020/network-faster-than-cache/#cache-will-not-save-us" aria-hidden="true">#</a></h3>
<p>There is an assumption that cached assets are retrieved instantly and at zero cost. What we have discovered here is that there is in fact a cost to retrieving assets from cache based on the number of cached assets (not file size) and the user's devices. Concatenating / bundling your assets is <strong>probably</strong> still a good practice, even on H/2 connections. Obviously this comes on balance with cache eviction costs and splitting pre- and post-load bundles. Make sure you test performance on real devices, and definitely start tracking <a href="https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/quick-start-quide#TOC-Repeat-View">repeat-view performance</a> of changes you make to bundling and cache configuration!</p>
<p>If nothing else, you might remember this the next time you're stumped by some strange cache behaviour 🙂</p>
<p>Let me know by interacting below if you have any thoughts on cache performance, or if you have ideas on other interesting dimensions to drill in to!</p>
Is it time for a Web Performance rebrand?2020-05-03T13:00:00Zhttps://simonhearne.com/2020/web-performance-rebrand/<p>Web performance has been a hot topic in web development and browser engineering since the first public web pages. The first book on the topic, <a href="https://www.amazon.co.uk/Web-Performance-Tuning-Speeding/dp/059600172X/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=webnin-21">Web Performance Tuning: Speeding Up the Web</a> was published in 1998, with Steve Souders kicking off the Web Performance Optimisation industry when he published his book <a href="https://www.amazon.co.uk/High-Performance-Web-Sites-Essential/dp/0596529309/ref=as_li_ss_tl?dchild=1&keywords=high+performance+websites&qid=1588522271&s=books&sr=1-1&linkCode=ll1&tag=webnin-21">High Performance Websites</a> nine years later in 2007.</p>
<p>As an industry-within-an-industry we have built resources, adopted hashtags (<a href="https://twitter.com/search?q=%23webperf">#WebPerf</a>, <a href="https://twitter.com/search?q=%23perfmatters">#PerfMatters</a>) and created meetups and conferences (<a href="https://ldnwebperf.org/">LDNWebPerf</a>, <a href="https://perfmattersconf.com/">#PerfMatters</a>, <a href="https://perfnow.nl/">Performance.now</a>). We've focused in on a subset of web development, whilst simultaneously complaining that it's hard to get business buy-in and wider appreciation of the topic. We've even <a href="https://wpostats.com/">created a site</a> to collect proof that web performance is important to business success! We know that web performance is important, but we consistently struggle to convince the wider world to prioritise speed as a feature.</p>
<p>Something that has long troubled me when discussing web performance with customers (and friends outside the industry) is that the term "web performance" is ambiguous, it means different things to different people. To someone in marketing it might mean the performance of web campaigns, measured in click-through rate and engagements. To ecommerce it might mean the business performance of the website, measured in conversions and revenue. To SEOs it could be the ranking of key pages, and to performance artists it means using the web as a medium for their art!</p>
<p>This ambiguity can work in our favour; it means our messaging lands with lots of different parties within a business: everyone wants to improve web performance! This often leads to confusion in the first meeting, requiring early clarification of what web performance means to us and our tools & services. I find myself describing <em>our</em> version of web performance as "measuring and optimising the speed of a web or mobile application to maximise user experience and business success".</p>
<blockquote>
<p>Web Performance is measuring and optimising the speed of a web or mobile application to maximise user experience and business success</p>
</blockquote>
<p>If we are to correctly engage the broadest possible audience with our messaging, we could do better with more targeted language. Should we be using a different term when generating marketing material and engaging with external stakeholders?</p>
<blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">What is <a href="https://twitter.com/hashtag/webperf?src=hash&ref_src=twsrc%5Etfw">#webperf</a> most commonly known as, outside of the web development industry? (E.g. Marketing, UX, Execs)</p>— Simon Hearne (@simonhearne) <a href="https://twitter.com/simonhearne/status/1256103539342000128?ref_src=twsrc%5Etfw">May 1, 2020</a></blockquote><script async="" src="https://simonhearne.com/js/twitter_widgets.js" charset="utf-8"></script>
<p>Site speed or page speed are often mentioned by executives and analysts. I think we can assume that Google is to thank here! Web performance stats in Google Analytics are under the <code>Site Speed</code> section, and <code>PageSpeed Insights</code> is the Google tool to <a href="https://developers.google.com/speed/pagespeed/insights/">find performance issues</a>. How often do we hear "We need to improve page speed" vs. "We need to improve web performance"?</p>
<p>Whilst certainly not scientific, a quick look on Google Trends shows that <code>page speed</code> is by far the most common search term, followed at some distance by <code>site speed</code>, with <code>web performance</code> barely registering at ~5% relative interest.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/rebrand2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/nJyO983_pp-600.avif 600w, https://simonhearne.com/img/nJyO983_pp-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/nJyO983_pp-600.webp 600w, https://simonhearne.com/img/nJyO983_pp-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/nJyO983_pp-600.jpeg 600w, https://simonhearne.com/img/nJyO983_pp-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Google trends chart showing page speed has the highest popularity, followed by site speed, with web performance only 5% relative interest" loading="lazy" decoding="async" src="https://simonhearne.com/img/nJyO983_pp-600.jpeg" width="900" height="484" /></picture></a><figcaption>Google Trends for top three web performance terms. <a href="https://trends.google.com/trends/explore?q=web%20performance,page%20speed,site%20speed">Link to original query.</a></figcaption></figure>
<p>Take a look at <a href="https://twitter.com/search?q=%23SiteSpeed">#SiteSpeed</a> on Twitter and you will see tweets from a range of SEO, business and Marketing folks, whereas the <a href="https://twitter.com/search?q=%23WebPerf">#WebPerf</a> hashtag is mainly technical tweets from folks in the industry, tools providers or front-end developers.</p>
<p>Web Performance and Web Performance Optimization are still valid and descriptive terms for our industry, but we might benefit from a change to our language when working with others. The language we use could be critical to the success of making the web a faster and more accessible place.</p>
<p>I'll be testing this out with my messaging to clients, I'd be interested in your thoughts too. Some other ideas from the community:</p>
<ul>
<li><strong>SSO</strong> (Site Speed Optimisation) - from <a href="https://twitter.com/aaronpeters">Aaron Peters</a></li>
<li><strong>UXSpeed</strong> - from <a href="https://twitter.com/sergeyche">Sergey Chernyshev</a></li>
<li><strong>WebSpeed</strong> - from <a href="https://twitter.com/simevidas">Šime Vidas</a></li>
</ul>
Collecting SmartThings data in InfluxDB on Raspberry Pi2020-01-07T13:45:00Zhttps://simonhearne.com/2020/pi-smartthings-influx/<p>Now that you've <a href="https://simonhearne.com/2020/pi-influx-grafana/">got your Pi set up with Influx & Grafana</a>, you're <a href="https://simonhearne.com/2020/pi-speedtest-influx/">collecting some system stats</a>, and <a href="https://simonhearne.com/2020/pi-speetest-influx/">measuring network performance</a>, it's time to collect data from SmartThings!</p>
<p><a href="https://www.samsung.com/uk/smartthings/">Samsung SmartThings</a> is a multi-protocol smart home hub produced by Samsung, it supports Z-Wave, Zigbee and Bluetooth and is surprisingly configurable. One of my main frustrations with the service is the lack of visibility into the huge amounts of data that it collects, so I decided to store that data myself! This way I can query it and visualise it in Grafana, set alerts and get insight into how my home is performing.</p>
<h2 id="step-0-set-up-influxdb-and-grafana" tabindex="-1">Step 0: Set up InfluxDB and Grafana <a class="direct-link" href="https://simonhearne.com/2020/pi-smartthings-influx/#step-0-set-up-influxdb-and-grafana" aria-hidden="true">#</a></h2>
<p>This post assumes that you have an InfluxDB and Grafana server running already, take a look at my post on <a href="https://simonhearne.com/2020/pi-influx-grafana/">getting a Raspberry Pi configured</a> if you need to get set up first.</p>
<h2 id="step-1-adding-the-smartapp" tabindex="-1">Step 1: Adding the SmartApp <a class="direct-link" href="https://simonhearne.com/2020/pi-smartthings-influx/#step-1-adding-the-smartapp" aria-hidden="true">#</a></h2>
<p>We need to install a SmartApp to SmartThings to collect the sensor data. This is a relatively straighforward process but is daunting the first time you do it.</p>
<ol>
<li>Log in to <a href="https://graph.api.smartthings.com/">the IDE</a></li>
<li>Navigate to the <code>My SmartApps</code> section</li>
<li>Click the blue <code>+ New SmartApp</code> button in the top right of the screen</li>
<li>Select the <code>From Code</code> tab</li>
<li>Copy and Paste the groovy code from <a href="https://raw.githubusercontent.com/codersaur/SmartThings/master/smartapps/influxdb-logger/influxdb-logger.groovy">influxdb-logger</a></li>
<li>Hit <code>Create</code> - you should see a green "Created SmartApp" banner</li>
<li>Click the <code>Publish -> For Me</code> button to make the app available</li>
</ol>
<h2 id="step-2-configure-the-logger" tabindex="-1">Step 2: Configure the Logger <a class="direct-link" href="https://simonhearne.com/2020/pi-smartthings-influx/#step-2-configure-the-logger" aria-hidden="true">#</a></h2>
<p>Now the SmartApp is enabled, you should see it in your SmartThings mobile application under SmartApps:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/pi-smartthings-influx/sm_2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/U4Xgld600j-600.avif 600w, https://simonhearne.com/img/U4Xgld600j-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/U4Xgld600j-600.webp 600w, https://simonhearne.com/img/U4Xgld600j-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/U4Xgld600j-600.jpeg 600w, https://simonhearne.com/img/U4Xgld600j-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="New InfluxDB Logger SmartApp." loading="lazy" decoding="async" src="https://simonhearne.com/img/U4Xgld600j-600.jpeg" width="900" height="478" /></picture></a><figcaption>New InfluxDB Logger SmartApp.</figcaption></figure>
<p>Click on the app to open the settings:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/pi-smartthings-influx/sm_3.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/8zMjyxdZmd-600.avif 600w, https://simonhearne.com/img/8zMjyxdZmd-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/8zMjyxdZmd-600.webp 600w, https://simonhearne.com/img/8zMjyxdZmd-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/8zMjyxdZmd-600.jpeg 600w, https://simonhearne.com/img/8zMjyxdZmd-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Configuring InfluxDB Settings." loading="lazy" decoding="async" src="https://simonhearne.com/img/8zMjyxdZmd-600.jpeg" width="900" height="1369" /></picture></a><figcaption>Configuring InfluxDB Settings.</figcaption></figure>
<p>Select all devices which you would like to log:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/pi-smartthings-influx/sm_4.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/_9JVuSuMQT-600.avif 600w, https://simonhearne.com/img/_9JVuSuMQT-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/_9JVuSuMQT-600.webp 600w, https://simonhearne.com/img/_9JVuSuMQT-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/_9JVuSuMQT-600.jpeg 600w, https://simonhearne.com/img/_9JVuSuMQT-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Selecting Devices to Log." loading="lazy" decoding="async" src="https://simonhearne.com/img/_9JVuSuMQT-600.jpeg" width="900" height="1026" /></picture></a><figcaption>Selecting Devices to Log.</figcaption></figure>
<p>Save the settings and make sure that the app is enabled, you'll see data in your InfluxDB immediately if everything is working correctly. You can debug by viewing the live logs of your SmartThings location - click on <code>Live Logging</code> in the menu of the SmartThings IDE:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/pi-smartthings-influx/sm_5.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/GZDB4lWCRl-600.avif 600w, https://simonhearne.com/img/GZDB4lWCRl-900.avif 900w, https://simonhearne.com/img/GZDB4lWCRl-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/GZDB4lWCRl-600.webp 600w, https://simonhearne.com/img/GZDB4lWCRl-900.webp 900w, https://simonhearne.com/img/GZDB4lWCRl-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/GZDB4lWCRl-600.jpeg 600w, https://simonhearne.com/img/GZDB4lWCRl-900.jpeg 900w, https://simonhearne.com/img/GZDB4lWCRl-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Viewing Live Logs in SmartThings." loading="lazy" decoding="async" src="https://simonhearne.com/img/GZDB4lWCRl-600.jpeg" width="1200" height="483" /></picture></a><figcaption>Viewing Live Logs in SmartThings.</figcaption></figure>
<p>With any luck you'll have an InfluxDB filling up with device data!</p>
Collecting SpeedTest results in Influx on Raspberry Pi2020-01-07T13:30:00Zhttps://simonhearne.com/2020/pi-speedtest-influx/<p>Now that you've <a href="https://simonhearne.com/2020/pi-influx-grafana/">got your Pi set up with Influx & Grafana</a> and you're <a href="https://simonhearne.com/2020/pi-metrics-influx/">collecting some system stats</a>, it's time to measure network performance!</p>
<p><a href="https://www.speedtest.net/">SpeedTest.net</a> by Ookla is probably the most popular connection testing services, and they handily <a href="https://www.speedtest.net/apps/cli">provide a CLI</a> to run tests programmatically. We'll create a simple python script which runs a test and sends the data to influxdb, then set it to run once every 15 minutes using Cron.</p>
<p>First, we need to install the <code>speedtest-cli</code> client:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> python-pip<br /><span class="token function">sudo</span> pip <span class="token function">install</span> speedtest-cli influxdb</code></pre>
<p>Then we can create a python script <code>rpi-speedtest-influx.py</code> in our home directory to run tests:</p>
<pre class="language-python"><code class="language-python"><span class="token comment">#!/usr/bin/env python</span><br /><br /><span class="token keyword">import</span> datetime<br /><span class="token keyword">import</span> speedtest<br /><span class="token keyword">from</span> influxdb <span class="token keyword">import</span> InfluxDBClient<br /><br /><span class="token comment"># influx configuration - edit these</span><br />ifuser <span class="token operator">=</span> <span class="token string">"grafana"</span><br />ifpass <span class="token operator">=</span> <span class="token string">"<yourpassword>"</span><br />ifdb <span class="token operator">=</span> <span class="token string">"home"</span><br />ifhost <span class="token operator">=</span> <span class="token string">"127.0.0.1"</span><br />ifport <span class="token operator">=</span> <span class="token number">8086</span><br />measurement_name <span class="token operator">=</span> <span class="token string">"speedtest"</span><br /><br /><span class="token comment"># take a timestamp for this measurement</span><br />time <span class="token operator">=</span> datetime<span class="token punctuation">.</span>datetime<span class="token punctuation">.</span>utcnow<span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># run a single-threaded speedtest using default server</span><br />s <span class="token operator">=</span> speedtest<span class="token punctuation">.</span>Speedtest<span class="token punctuation">(</span><span class="token punctuation">)</span><br />s<span class="token punctuation">.</span>get_best_server<span class="token punctuation">(</span><span class="token punctuation">)</span><br />s<span class="token punctuation">.</span>download<span class="token punctuation">(</span>threads<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span><br />s<span class="token punctuation">.</span>upload<span class="token punctuation">(</span>threads<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span><br />res <span class="token operator">=</span> s<span class="token punctuation">.</span>results<span class="token punctuation">.</span><span class="token builtin">dict</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># format the data as a single measurement for influx</span><br />body <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token string">"measurement"</span><span class="token punctuation">:</span> measurement_name<span class="token punctuation">,</span><br /> <span class="token string">"time"</span><span class="token punctuation">:</span> time<span class="token punctuation">,</span><br /> <span class="token string">"fields"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"download"</span><span class="token punctuation">:</span> res<span class="token punctuation">[</span><span class="token string">"download"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"upload"</span><span class="token punctuation">:</span> res<span class="token punctuation">[</span><span class="token string">"upload"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"ping"</span><span class="token punctuation">:</span> res<span class="token punctuation">[</span><span class="token string">"ping"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">]</span><br /><br /><span class="token comment"># connect to influx</span><br />ifclient <span class="token operator">=</span> InfluxDBClient<span class="token punctuation">(</span>ifhost<span class="token punctuation">,</span>ifport<span class="token punctuation">,</span>ifuser<span class="token punctuation">,</span>ifpass<span class="token punctuation">,</span>ifdb<span class="token punctuation">)</span><br /><br /><span class="token comment"># write the measurement</span><br />ifclient<span class="token punctuation">.</span>write_points<span class="token punctuation">(</span>body<span class="token punctuation">)</span></code></pre>
<p><a href="https://gist.github.com/simonhearne/eabf033bae8cee77972b9e96d2775eea#file-rpi-speedtest-influx-py">View gist on GitHub</a></p>
<p>We'll need to make it executable: <code>chmod +x rpi-speedtest-influx.py</code> and then we can run it to test it out: <code>./rpi-speedtest-influx.py</code>. You should be able to see the measurement in influx as soon as the script has exited, check by running <code>influx</code> CLI and execute the following to check your measurement (swapping database and measurement names for those you defined in your python script):</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">use</span> home<br /><span class="token keyword">select</span> <span class="token operator">*</span> <span class="token keyword">from</span> speedtest</code></pre>
<p>If everything looks ok, we can add a cron job for our user (using <code>crontab -e</code>) to run this script at a regular interval, e.g. every 15 minutes: <code>*/15 * * * * /home/pi/rpi-speedtest-influx.py</code>. Any more than 15 minutes is probably unnecessary and may have an adverse effect on your perceived network quality.</p>
<p>You can now simply add a panel to your Grafana instance to show the download, upload and ping results over time. <a href="https://gist.github.com/simonhearne/1b2b62e818fe6e097f8f8b2cbd6f2365">Here is a sample dashboard</a> you can import which includes the panel shown below:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/grafana-net-stats.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/9JZSg4qqqt-600.avif 600w, https://simonhearne.com/img/9JZSg4qqqt-900.avif 900w, https://simonhearne.com/img/9JZSg4qqqt-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/9JZSg4qqqt-600.webp 600w, https://simonhearne.com/img/9JZSg4qqqt-900.webp 900w, https://simonhearne.com/img/9JZSg4qqqt-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/9JZSg4qqqt-600.jpeg 600w, https://simonhearne.com/img/9JZSg4qqqt-900.jpeg 900w, https://simonhearne.com/img/9JZSg4qqqt-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="SpeedTest Grafana dashboard." loading="lazy" decoding="async" src="https://simonhearne.com/img/9JZSg4qqqt-600.jpeg" width="1200" height="366" /></picture></a><figcaption>SpeedTest Grafana dashboard.</figcaption></figure>
Reporting Raspberry Pi System Metrics to InfluxDB2020-01-07T13:15:00Zhttps://simonhearne.com/2020/pi-metrics-influx/<p>Now that you've <a href="https://simonhearne.com/2020/pi-influx-grafana/">got your Pi set up with Influx & Grafana</a>, it's time to start collecting some data! We will use a simple Python script to collect system statistics and push it to Influx - you can use this as-is or modify it to your needs.</p>
<p>We will need to first install the PIP package manager if it is not already on your Pi:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> python-pip</code></pre>
<p>And then use pip to install the python packages <code>psutil</code> and <code>influxdb</code>:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> pip <span class="token function">install</span> psutil influxdb</code></pre>
<p>The python script below will use <code>psutil</code> to collect some system stats and <code>influx</code> to insert them into your influx db:</p>
<pre class="language-python"><code class="language-python"><span class="token comment">#!/usr/bin/env python</span><br /><br /><span class="token keyword">import</span> datetime<br /><span class="token keyword">import</span> psutil<br /><span class="token keyword">from</span> influxdb <span class="token keyword">import</span> InfluxDBClient<br /><br /><span class="token comment"># influx configuration - edit these</span><br />ifuser <span class="token operator">=</span> <span class="token string">"grafana"</span><br />ifpass <span class="token operator">=</span> <span class="token string">"<yourpassword>"</span><br />ifdb <span class="token operator">=</span> <span class="token string">"home"</span><br />ifhost <span class="token operator">=</span> <span class="token string">"127.0.0.1"</span><br />ifport <span class="token operator">=</span> <span class="token number">8086</span><br />measurement_name <span class="token operator">=</span> <span class="token string">"system"</span><br /><br /><span class="token comment"># take a timestamp for this measurement</span><br />time <span class="token operator">=</span> datetime<span class="token punctuation">.</span>datetime<span class="token punctuation">.</span>utcnow<span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># collect some stats from psutil</span><br />disk <span class="token operator">=</span> psutil<span class="token punctuation">.</span>disk_usage<span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span><br />mem <span class="token operator">=</span> psutil<span class="token punctuation">.</span>virtual_memory<span class="token punctuation">(</span><span class="token punctuation">)</span><br />load <span class="token operator">=</span> psutil<span class="token punctuation">.</span>getloadavg<span class="token punctuation">(</span><span class="token punctuation">)</span><br /><br /><span class="token comment"># format the data as a single measurement for influx</span><br />body <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token string">"measurement"</span><span class="token punctuation">:</span> measurement_name<span class="token punctuation">,</span><br /> <span class="token string">"time"</span><span class="token punctuation">:</span> time<span class="token punctuation">,</span><br /> <span class="token string">"fields"</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><br /> <span class="token string">"load_1"</span><span class="token punctuation">:</span> load<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"load_5"</span><span class="token punctuation">:</span> load<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"load_15"</span><span class="token punctuation">:</span> load<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token string">"disk_percent"</span><span class="token punctuation">:</span> disk<span class="token punctuation">.</span>percent<span class="token punctuation">,</span><br /> <span class="token string">"disk_free"</span><span class="token punctuation">:</span> disk<span class="token punctuation">.</span>free<span class="token punctuation">,</span><br /> <span class="token string">"disk_used"</span><span class="token punctuation">:</span> disk<span class="token punctuation">.</span>used<span class="token punctuation">,</span><br /> <span class="token string">"mem_percent"</span><span class="token punctuation">:</span> mem<span class="token punctuation">.</span>percent<span class="token punctuation">,</span><br /> <span class="token string">"mem_free"</span><span class="token punctuation">:</span> mem<span class="token punctuation">.</span>free<span class="token punctuation">,</span><br /> <span class="token string">"mem_used"</span><span class="token punctuation">:</span> mem<span class="token punctuation">.</span>used<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">]</span><br /><br /><span class="token comment"># connect to influx</span><br />ifclient <span class="token operator">=</span> InfluxDBClient<span class="token punctuation">(</span>ifhost<span class="token punctuation">,</span>ifport<span class="token punctuation">,</span>ifuser<span class="token punctuation">,</span>ifpass<span class="token punctuation">,</span>ifdb<span class="token punctuation">)</span><br /><br /><span class="token comment"># write the measurement</span><br />ifclient<span class="token punctuation">.</span>write_points<span class="token punctuation">(</span>body<span class="token punctuation">)</span></code></pre>
<p><a href="https://gist.github.com/simonhearne/7a8ef5a792050b89bd708fb754be0bb4#file-rpi-stats-influx-py">View gist on GitHub</a></p>
<p>Create this script in your home directory, e.g. <code>/home/pi/rpi-stats-influx.py</code> and make the script executable with <code>chmod +x rpi-stats-influx.py</code>.</p>
<p>Test the script out by running it with <code>./rpi-stats-influx.py</code> and check that no errors are thrown. If all is good, we can set the script to run reqularly using <code>cron</code></p>
<p>Run <code>crontab -e</code> to edit your user's crontab and add an entry <code>* * * * * /home/pi/rpi-stats-influx.py</code> to the bottom of the file, this will run the script every minute. The script is very lightweight so running every minute should be fine.</p>
<p>See <a href="https://psutil.readthedocs.io/en/latest/">the docs for <code>psutil</code></a> to get ideas for other stats you can collect.</p>
<p>You can now simply add a panel to your Grafana instance to show your system stats over time. <a href="https://gist.github.com/simonhearne/e4fd4d9f94c302a5847324a48724c6eb">Here is a sample dashboard</a> you can import which includes the panel shown below:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/grafana-pi-stats.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/BFaMLSXSan-600.avif 600w, https://simonhearne.com/img/BFaMLSXSan-900.avif 900w, https://simonhearne.com/img/BFaMLSXSan-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/BFaMLSXSan-600.webp 600w, https://simonhearne.com/img/BFaMLSXSan-900.webp 900w, https://simonhearne.com/img/BFaMLSXSan-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/BFaMLSXSan-600.jpeg 600w, https://simonhearne.com/img/BFaMLSXSan-900.jpeg 900w, https://simonhearne.com/img/BFaMLSXSan-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Pi Stats Grafana dashboard." loading="lazy" decoding="async" src="https://simonhearne.com/img/BFaMLSXSan-600.jpeg" width="1200" height="370" /></picture></a><figcaption>Pi Stats Grafana dashboard.</figcaption></figure>
Installing InfluxDB & Grafana on Raspberry Pi2020-01-07T13:00:00Zhttps://simonhearne.com/2020/pi-influx-grafana/<h2 id="step-0-initial-setup" tabindex="-1">Step 0: Initial Setup <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-0-initial-setup" aria-hidden="true">#</a></h2>
<p>Follow these steps first if you have a brand new rPi:</p>
<ol>
<li>
<p>Download the latest <em>lite</em> Raspbian image from <a href="https://www.raspberrypi.org/downloads/raspbian/">raspberrypi.org</a></p>
</li>
<li>
<p>Get a reasonable micro SD card - 32GB is the maximum supported size. I use a <a href="https://amzn.to/37DqONM">SanDisk Ultra</a></p>
</li>
<li>
<p>Burn the image to your SD card, I recommend using <a href="https://www.balena.io/etcher/">balenaEtcher</a>.</p>
</li>
<li>
<p>Balena will eject your SD card once it is complete, re-mount your card by physically removing and re-inserting it, then create an empty file called <code>ssh</code> in the root of the SD card - this enables SSH access which we'll need later.</p>
</li>
<li>
<p>Insert the SD card into your Pi, connect to your router via ethernet and power on.</p>
</li>
<li>
<p>Determine the auto-assigned IP address of the Pi by logging in to your router interface (see a guide on finding your router IP address <a href="https://nordvpn.com/blog/find-router-ip-address/">here</a>) and navigating to LAN / DHCP settings - the pi should be recognised as <code>raspberrypi</code>. Now is a good time to assign a static IP for your Pi to make life easier in the future. You'll need to power cycle your Pi if you change from the auto assigned IP address.</p>
</li>
<li>
<p>SSH into the Pi using the IP address you have determined / assigned: <code>ssh pi@<yourip></code>. You should probably update the password for the <code>pi</code> user now by running <code>passwd</code>.</p>
</li>
<li>
<p>(optional!) put your Pi in a case to keep it safe and cool. I found a cheap (£6 / $8) <a href="https://amzn.to/2rXT924">case with heatsinks and fan</a> on Amazon.</p>
</li>
</ol>
<h2 id="step-1-getting-up-to-date" tabindex="-1">Step 1: Getting up to date <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-1-getting-up-to-date" aria-hidden="true">#</a></h2>
<p>First off we'll make sure everything is up to date. This could take a while, especially on a new Pi:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update<br /><span class="token function">sudo</span> <span class="token function">apt</span> upgrade <span class="token parameter variable">-y</span></code></pre>
<h2 id="step-2-install-influxdb" tabindex="-1">Step 2: Install Influxdb <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-2-install-influxdb" aria-hidden="true">#</a></h2>
<p>First we add Influx repositories to apt:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> -qO- https://repos.influxdata.com/influxdb.key <span class="token operator">|</span> <span class="token function">sudo</span> apt-key <span class="token function">add</span> -<br /><span class="token builtin class-name">source</span> /etc/os-release<br /><span class="token builtin class-name">echo</span> <span class="token string">"deb https://repos.influxdata.com/debian <span class="token variable"><span class="token variable">$(</span>lsb_release <span class="token parameter variable">-cs</span><span class="token variable">)</span></span> stable"</span> <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> /etc/apt/sources.list.d/influxdb.list</code></pre>
<p>Update apt with the new repos, & install.</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update <span class="token operator">&&</span> <span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> influxdb</code></pre>
<p>Then start the <code>influxdb</code> service and set it to run at boot:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> systemctl unmask influxdb.service<br /><span class="token function">sudo</span> systemctl start influxdb<br /><span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> influxdb.service</code></pre>
<p>We should now be able to run the influx client with <code>influx</code> and create a user for later (here I use a single admin user <code>grafana</code> for simplicity):</p>
<pre class="language-bash"><code class="language-bash">create database home<br />use home<br /><br />create user grafana with password <span class="token string">'<passwordhere>'</span> with all privileges<br />grant all privileges on home to grafana<br /><br />show <span class="token function">users</span><br /><br />user admin<br />---- -----<br />grafana <span class="token boolean">true</span></code></pre>
<p>That's it! You can now exit the Influx client by typing <code>exit</code>.</p>
<h2 id="step-3-install-grafana" tabindex="-1">Step 3: Install Grafana <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-3-install-grafana" aria-hidden="true">#</a></h2>
<p>Again we need to add the Grafana packages to apt:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> <span class="token parameter variable">-q</span> <span class="token parameter variable">-O</span> - https://packages.grafana.com/gpg.key <span class="token operator">|</span> <span class="token function">sudo</span> apt-key <span class="token function">add</span> -<br /><span class="token builtin class-name">echo</span> <span class="token string">"deb https://packages.grafana.com/oss/deb stable main"</span> <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> /etc/apt/sources.list.d/grafana.list</code></pre>
<p>We can now update and install the binaries:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update <span class="token operator">&&</span> <span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> grafana</code></pre>
<p>Then simply enable the service and set to run at boot:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">sudo</span> systemctl unmask grafana-server.service<br /><span class="token function">sudo</span> systemctl start grafana-server<br /><span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> grafana-server.service</code></pre>
<p>Now we can check that grafana is up by loading it in a browser: <code>http://<ipaddress>:3000</code>. If so, you can log in with the username and password = <code>admin</code> and set a new admin password.</p>
<h2 id="step-4-add-influx-as-a-grafana-data-source" tabindex="-1">Step 4: Add Influx as a Grafana data source <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-4-add-influx-as-a-grafana-data-source" aria-hidden="true">#</a></h2>
<p>Now we have both Influx and Grafana running, we can stitch them together. Log in to your Grafana instance and head to "Data Sources". Select "Add new Data Source" and find InfluxDB under "Timeseries Databases".</p>
<p>As we are running both services on the same Pi, set the URL to localhost and use the default influx port of <code>8086</code>:</p>
<p><img src="https://simonhearne.com/images/grafana1.png" alt="Grafana configuration for influxdb 1" /></p>
<p>We then need to add the database, user and password that we set earlier:</p>
<p><img src="https://simonhearne.com/images/grafana2.png" alt="Grafana configuration for influxdb 2" /></p>
<p>That's all we need! Now go ahead and hit "Save & Test" to connect everything together:</p>
<p><img src="https://simonhearne.com/images/grafana3.png" alt="Grafana configuration for influxdb 3" /></p>
<h2 id="step-5-collecting-some-data" tabindex="-1">Step 5: Collecting some data <a class="direct-link" href="https://simonhearne.com/2020/pi-influx-grafana/#step-5-collecting-some-data" aria-hidden="true">#</a></h2>
<p>Now you've got your Influx & Grafana setup running, you can <a href="https://simonhearne.com/2020/pi-metrics-influx">collect system stats</a>, run <a href="https://simonhearne.com/2020/pi-speedtest-influx">regular network speed tests</a> and collect <a href="https://simonhearne.com/2020/pi-smartthings-influx">data from your home automation system</a>!</p>
HTTP Headers for fast & secure static sites2019-12-10T00:00:00Zhttps://simonhearne.com/2019/http-headers-fast-and-secure/<p>This website is powered by Netlify, it also has Content-Security-Policy and cache-control HTTP response headers to improve client security and performance. In this short article I'll describe the key response headers you should be aware of and how how I configured them on Netlify. The configuration is simple when you know how, but it can take a while to work out!</p>
<h2 id="intro-to-http-headers" tabindex="-1">Intro to HTTP Headers <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#intro-to-http-headers" aria-hidden="true">#</a></h2>
<p>I am discussing HTTP response headers in this article. These are a set of key-value pairs sent by a web server at the beginning of a response, separated by a colon. The headers are directives which tell HTTP clients how to treat responses (where clients can be CDNs, proxies and web browsers). No headers are required by the HTTP protocol, but <code>status</code> is a requirement for an HTTP response.</p>
<p>You can see response headers in your browser developer tools, just select a response and view the headers:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/UcM052wPkY-600.avif 600w, https://simonhearne.com/img/UcM052wPkY-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/UcM052wPkY-600.webp 600w, https://simonhearne.com/img/UcM052wPkY-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/UcM052wPkY-600.jpeg 600w, https://simonhearne.com/img/UcM052wPkY-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Screenshot of response headers from chrome developer tools" loading="lazy" decoding="async" src="https://simonhearne.com/img/UcM052wPkY-600.jpeg" width="900" height="534" /></picture><figcaption>Response headers shown in Chrome Developer Tools for a page on this site.</figcaption></figure>
<p>In the following sections we will walk through some of the key use cases for response headers, when to use them and what they look like.</p>
<h3 id="content" tabindex="-1">Content <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#content" aria-hidden="true">#</a></h3>
<p>Content headers are critical for content that can be negotiated between client and server, such as image formats and encoding algorithms. The following response headers indicate that the response body is binary encoded using gzip, and that the content is an HTML document with a UTF8 charset:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Content-Encoding</span><span class="token punctuation">:</span> <span class="token header-value">gzip</span></span><br /><span class="token header"><span class="token header-name keyword">Content-Type</span><span class="token punctuation">:</span> <span class="token header-value">text/html; charset=utf-8</span></span></code></pre>
<p>Thankfully your CMS or CDN should take care of these for you. One thing to look out for is support for gzip and brotli compression. If there is no <code>Content-Encoding</code> header on text files (HTML, CSS, JS etc.) than your static assets are being sent uncompressed. Some providers (such as <a href="https://devcenter.heroku.com/articles/compressing-http-messages-with-gzip">Heroku</a>) do not compress content for you, so you'll need to implement it in your application.</p>
<h3 id="caching" tabindex="-1">Caching <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#caching" aria-hidden="true">#</a></h3>
<p>There are a wide range of response headers which impact how browsers cache content. Caching static content is important for delivering fast page loads: the fastest request is the one that is never made. Some content should never be cached, however, such as responses from a login or balance API or pages with very dynamic content. Response headers provide fine-grained control over how HTTP clients cache content. Caching headers should be present on every response, below are some examples of the Cache-Control header for a number of scenarios:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">public, max-age=31536000, immutable # any cache can store this for a year use it without revalidating</span></span><br /><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">public, max-age=2628000 # any cache can store this for a month</span></span><br /><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">private, max-age=3600 # the browser may store this for one hour</span></span><br /><span class="token header"><span class="token header-name keyword">Cache-Control</span><span class="token punctuation">:</span> <span class="token header-value">no-store # no cache may store this at all</span></span></code></pre>
<p>Unfortunately, <a href="https://www.netlify.com/blog/2017/02/23/better-living-through-caching/">Netlify defaults to</a> <code>Cache-Control: max-age=0, must-revalidate, public</code> and an <code>ETag</code> header on all responses, instructing caches not to serve cached content without revalidating against the origin that asset has not changed. This can have a negative impact on performance, especially for users on high-latency connections, as a conditional GET request will be made for all resources. It does, however, mean that changes you make to your site are reflected on the web almost immediately. You can override default Netlify behaviour by setting the response header to blank, e.g. <code>Etag = ""</code>. This might be particularly useful to prevent revalidation on assets such as webfonts, to prevent a flash of <a href="https://css-tricks.com/fout-foit-foft/">unstyled or invisible text</a> while the page renders and revalidates the cached font files.</p>
<p>There are a bunch of headers which impact caching: <code>Etag</code>, <code>Age</code>, <code>Last-Modified</code>, <code>Expires</code> and <code>Pragma</code> are all considered by the browsers when determining cache state, although <code>Cache-Control</code> supersedes some behaviours such as <code>Expires</code>. Further reading: <a href="https://www.keycdn.com/blog/http-cache-headers">HTTP Cache Headers on Key CDN</a>.</p>
<h3 id="content-security-policy" tabindex="-1">Content Security Policy <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#content-security-policy" aria-hidden="true">#</a></h3>
<p>There are a number of headers which enable security features in browsers. The most exciting (for me) header is Content-Security-Policy (CSP), this gives the browser a list of approved content sources, and what they are allowed to do.</p>
<p>A number of recent public cases of <a href="https://threatpost.com/magecart-ecommerce-card-skimming-bonanza/147765/">Magecart attacks</a>, data skimming and compromised third-parties make the CSP header a must-have for any transactional site. CSP will also block any malicious browser extensions from injecting ads onto your pages which slow the experience down. There is no reason to not set up the CSP header on your static site.</p>
<p>CSP directives align roughly with HTML5 and JavaScript functionality, a full list of directives is included below:</p>
<style>
table {width: fit-content;margin: 1em auto}
table th {text-align: left}
table td:first-child, th:first-child {text-align: right; padding-right: 1em}
table tr:nth-child(even) {background-color: rgba(128,128,128,0.1)}
</style>
<table>
<thead>
<tr>
<th>CSP Directive</th>
<th>HTML / JS Features</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>default-src</code></td>
<td><code>*</code></td>
</tr>
<tr>
<td><code>connect-src</code></td>
<td><code>fetch()</code>, <code>WebSocket()</code>, etc.</td>
</tr>
<tr>
<td><code>style-src</code></td>
<td><code><link rel="stylesheet"></code></td>
</tr>
<tr>
<td><code>script-src</code></td>
<td><code><script></code></td>
</tr>
<tr>
<td><code>form-action</code></td>
<td><code><form></code></td>
</tr>
<tr>
<td><code>font-src</code></td>
<td><code>@font-face</code></td>
</tr>
<tr>
<td><code>child-src</code></td>
<td><code><iframe></code>, <code>Worker()</code></td>
</tr>
<tr>
<td><code>object-src</code></td>
<td><code><object></code>, <code><embed></code></td>
</tr>
<tr>
<td><code>media-src</code></td>
<td><code><video></code>, <code><audio></code></td>
</tr>
<tr>
<td><code>img-src</code></td>
<td><code><img></code></td>
</tr>
<tr>
<td><code>manifest-src</code></td>
<td><code><link rel="manifest"></code></td>
</tr>
</tbody>
</table>
<p>CSP also allows violations reports to be sent to specified URLs, using a deprecated <code>report-uri</code> directive and its replacement, <a href="https://developers.google.com/web/updates/2018/09/reportingapi">the Reporting API</a>, via a <code>report-to</code> header. CSP headers should <em>only</em> be sent on document responses, i.e. the HTML file.</p>
<p>An example CSP directive for this site is shown below. It allows some inline scripts (such as the Google Analytics snippet), scripts from Twitter and embedded content from <a href="http://noti.st/">noti.st</a>. Al other content will be blocked, and a CSP violation report will be sent to <a href="http://report-uri.com/">report-uri.com</a> for analysis.</p>
<pre class="language-http"><code class="language-http">default-src 'self';<br /> script-src 'self' 'sha256-6/iD6t0SQvujSE2Zwae43Lq7XJSEA98rpBEWsYJd5RU=' platform.twitter.com ...;<br /> img-src 'self' data: simonhearne.com *.google-analytics.com *.twitter.com *.twimg.com res.cloudinary.com caniuse.bitsofco.de;<br /> connect-src 'self';<br /> style-src 'self' 'unsafe-inline' platform.twitter.com;<br /> child-src 'self' *.twitter.com noti.st caniuse.bitsofco.de;<br /> worker-src 'self';<br /> report-uri https://simonhearne.report-uri.com/a/d/g;<br /> report-to report-uri;</code></pre>
<p>Inline scripts are generally a bad idea, but CSP allows us to define which should be allowed to excute by either providing a <code>nonce</code> - a unique identifier, or a hash of the script content. As pointed out by Amier Saleh <a href="https://twitter.com/AmierOSaleh/status/1206706768555855872?s=20">on twitter</a>, it is important not to use <code>nonce</code> on static sites, otherwise an attacker can simply copy the nonce string to inject their <code><script></code>! As such, we must use hashes for inline scripts. Helpfully, Chrome will tell you what the hash should be when it finds an inline script which violates your policy:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/WZiLT55HsU-600.avif 600w, https://simonhearne.com/img/WZiLT55HsU-900.avif 900w, https://simonhearne.com/img/WZiLT55HsU-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/WZiLT55HsU-600.webp 600w, https://simonhearne.com/img/WZiLT55HsU-900.webp 900w, https://simonhearne.com/img/WZiLT55HsU-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/WZiLT55HsU-600.jpeg 600w, https://simonhearne.com/img/WZiLT55HsU-900.jpeg 900w, https://simonhearne.com/img/WZiLT55HsU-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Screenshot of suggested hash value from chrome developer tools" loading="lazy" decoding="async" src="https://simonhearne.com/img/WZiLT55HsU-600.jpeg" width="1200" height="120" /></picture><figcaption>Suggested hash value shown in Chrome Developer Tools for inline script.</figcaption></figure>
<p>Further reading: <a href="https://www.keycdn.com/blog/http-security-headers">HTTP Security Headers on keycdn</a> and <a href="https://csp.withgoogle.com/docs/faq.html">Google's CSP FAQs</a></p>
<h3 id="hsts" tabindex="-1">HSTS <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#hsts" aria-hidden="true">#</a></h3>
<p>HTTP Strict Transport Security (HSTS) is a simple header which tells a browser that your site should not be accessed via non-encrypted (i.e. <code>http</code>) connections. This prevents https downgrade attacks and ensures that your visitors' information is sent securely.</p>
<p>While services like Netlify and CloudFlare offer https upgrade via redirects, the <code>hsts</code> header is a belt-and-braces approach to ensure that https is always used. This will prevent potential man-in-the-middle attacks which attempt to steal cookie data by making requests to non-existent non-secure subdomains. All non-secure requests will be automatically upgraded to https <em>before</em> the requests are made, preventing any data being accidentally sent over plaintext.</p>
<p>If you enable an HSTS header, ensure that your HTTPS configuration is correct! Browsers which have seen the header will refuse to connect over http for the duration of the max-age directive, including if the certificate is not trusted for any reason. Because of the risk to the availability of your site, it may be worth using a short max-age directive. Below is an example which tells the browser to enforce HTTPS connections across all subdomains for 24 hours. The recommended max-age is two years. A <code>preload</code> directive may be added to the header, which hints to browsers that this domain should always be accessed over https, this can be important as HSTS headers are only valid on sites served over secure connections.</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Strict-Transport-Security</span><span class="token punctuation">:</span> <span class="token header-value hsts languages-hsts">max-age=86400; includeSubDomains</span></span></code></pre>
<p>Further reading: <a href="https://www.troyhunt.com/understanding-http-strict-transport/">Understanding HSTS by Troy Hunt</a> and <a href="https://hstspreload.org/">HSTSPreload.org</a></p>
<h3 id="network-error-logging" tabindex="-1">Network Error Logging <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#network-error-logging" aria-hidden="true">#</a></h3>
<p>Network Error Logging (NEL) instructs browsers to send reports to a defined endpoint when it fails to load a page. NEL reports are sent in a number of situations, from DNS resolution failures to 5xx and 4xx server responses. This opens a whole new world of reporting, some of which has previously been a black-box to website owners. Note in the example below I have added a Reporting API header which defines the reporting group for the NEL header to use. NEL headers should <em>only</em> be sent on document responses, i.e. the HTML file.</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Report-To</span><span class="token punctuation">:</span> <span class="token header-value">{"group":"report-uri","max_age":31536000,"endpoints":[{"url":"https://simonhearne.report-uri.com/a/d/g"}],"include_subdomains":true}</span></span><br /><span class="token header"><span class="token header-name keyword">NEL</span><span class="token punctuation">:</span> <span class="token header-value">{"report_to":"report-uri","max_age":31536000,"include_subdomains":true}</span></span></code></pre>
<p>Further reading: <a href="https://scotthelme.co.uk/network-error-logging-deep-dive/">Network Error Logging: Deep Dive by Scott Helme</a></p>
<h3 id="feature-policy" tabindex="-1">Feature Policy <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#feature-policy" aria-hidden="true">#</a></h3>
<p>Feature Policy is like CSP, but for web platform features. This header allows you to disable access to features such as the camera, microphone and geolocation on your site. This prevents any malicious attempts to use these features by third-party scripts.</p>
<p>A reasonable assumption can be made that you know whether any part of your site needs access to these features, and if not you should set a feature policy which disables access:</p>
<pre class="language-http"><code class="language-http">Feature-Policy : camera 'none'; geolocation 'none'; microphone 'none'</code></pre>
<p>Policy headers can be split out, and like CSP you can limit access to the parent page or to specific domains:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Feature-Policy</span><span class="token punctuation">:</span> <span class="token header-value">unsized-media 'none'</span></span><br /><span class="token header"><span class="token header-name keyword">Feature-Policy</span><span class="token punctuation">:</span> <span class="token header-value">geolocation 'self' https://example.com</span></span><br /><span class="token header"><span class="token header-name keyword">Feature-Policy</span><span class="token punctuation">:</span> <span class="token header-value">camera *;</span></span></code></pre>
<p>Further reading: <a href="https://developers.google.com/web/updates/2018/06/feature-policy">Introduction to Feature Policy on Google Web Fundamentals</a></p>
<h3 id="client-hints" tabindex="-1">Client Hints <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#client-hints" aria-hidden="true">#</a></h3>
<p>Client Hints instruct the browser to send information about itself along with requests. This includes information about the current connection, device memory, screen resolution and whether the user has opted in to features such as Lite Pages. This can greatly simplify the deployment of responsive images and dynamic content based on device capability. I do not currently use this data for anything dynamic, but it is trivial to see how this can be integrated into an image delivery solution. Client hint headers should <em>only</em> be sent on document responses, i.e. the HTML file.</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">Accept-CH</span><span class="token punctuation">:</span> <span class="token header-value">Downlink,RTT,Device-Memory,Save-Data,DPR,Width</span></span></code></pre>
<p>Further reading: <a href="https://developers.google.com/web/updates/2015/09/automating-resource-selection-with-client-hints">Automating Resource Selection with Client Hints on Google Web Fundamentals</a></p>
<h3 id="custom" tabindex="-1">Custom <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#custom" aria-hidden="true">#</a></h3>
<p>Headers generally have to match the defined set of header keys, but there is an allowance for custom headers. All custom headers must be prefixed with <code>x-</code>, but then can be anything you like. These are often used to include debug information, such as which server served the content, what policies were applied and de facto standard headers such as <code>X-Forwarded-For</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For">used by CDNs</a>. If you have a <a href="http://wordpress.com/">wordpress.com</a> site, you may see these humorous headers:</p>
<pre class="language-http"><code class="language-http"><span class="token header"><span class="token header-name keyword">x-hacker</span><span class="token punctuation">:</span> <span class="token header-value">If you're reading this, you should visit automattic.com/jobs and apply to join the fun, mention this header.</span></span><br /><span class="token header"><span class="token header-name keyword">x-nananana</span><span class="token punctuation">:</span> <span class="token header-value">Batcache</span></span></code></pre>
<h2 id="configuring-headers-on-netlify" tabindex="-1">Configuring Headers on Netlify <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#configuring-headers-on-netlify" aria-hidden="true">#</a></h2>
<p>So now we know what headers are used for, let's configure them for a Netlify site. I'll use the <code>netlify.toml</code> <a href="https://docs.netlify.com/configure-builds/file-based-configuration/">file-based configuration</a> in this example, but it could be translated easily in to the <code>_headers</code> <a href="https://docs.netlify.com/routing/headers/">configuration</a>. Unfortunately, there is <a href="https://community.netlify.com/t/setting-response-headers-only-on-documents/">currently no way</a> to set a header on a document but not on static assets. As such, we have to set our security headers on all responses. This is not ideal, and in the H/1.1 world would be untenable due to the overhead of the header data. Luckily, H/2 <a href="https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/">compresses header data</a> and uses a shared dictionary which reduces the impact of this Netlify limitation. If you have Cloudflare in front of your Netlify configuration, <a href="https://gist.github.com/simonhearne/2715250eaccb911ebfff9e2315b2ffee">this worker script</a> will strip unnecessary headers from static assets.</p>
<p>Note that header values which include double quotes must be escaped with toml multiline string markers: triple single quotes <code>'''</code>.</p>
<pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br /><br /><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">headers</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br /> <span class="token comment"># Set the default header to the one we want for documents</span><br /> <span class="token key property">for</span> <span class="token punctuation">=</span> <span class="token string">"/*"</span><br /> <span class="token punctuation">[</span><span class="token table class-name">headers.values</span><span class="token punctuation">]</span><br /> <span class="token key property">cache-control</span> <span class="token punctuation">=</span> <span class="token string">"public,max-age=60"</span><br /> <span class="token key property">Referrer-Policy</span> <span class="token punctuation">=</span> <span class="token string">"no-referrer-when-downgrade"</span><br /> <span class="token key property">X-Frame-Options</span> <span class="token punctuation">=</span> <span class="token string">"SAMEORIGIN"</span><br /> <span class="token key property">X-Content-Type-Options</span> <span class="token punctuation">=</span> <span class="token string">"nosniff"</span><br /> <span class="token key property">Feature-Policy</span> <span class="token punctuation">=</span> <span class="token string">"camera 'none'; geolocation 'none'; microphone 'none'"</span><br /> <span class="token key property">Strict-Transport-Security</span> <span class="token punctuation">=</span> <span class="token string">"max-age=2592000; includeSubDomains"</span><br /> <span class="token key property">Accept-CH</span> <span class="token punctuation">=</span> <span class="token string">"Downlink,RTT,Device-Memory,Save-Data,DPR,Width"</span><br /> <span class="token key property">Report-To</span> <span class="token punctuation">=</span> <span class="token string">'''{<br /> "group":"report-uri",<br /> "max_age":31536000,<br /> "endpoints":[<br /> {"url":"https://simonhearne.report-uri.com/a/d/g"}<br /> ],"include_subdomains":true}'''</span><br /> <span class="token key property">NEL</span> <span class="token punctuation">=</span> <span class="token string">'''{<br /> "report_to":"report-uri",<br /> "max_age":31536000,<br /> "include_subdomains":true<br /> }'''</span><br /> <span class="token key property">Content-Security-Policy</span> <span class="token punctuation">=</span> <span class="token string">'''<br /> default-src 'self';<br /> script-src 'self' 'sha256-i+rYvjE2MLQRpsO7Qygbp0RqJCgJE9pO2xyOIhf5LZE=' www.google-analytics.com ...;<br /> img-src 'self' data: simonhearne.com *.google-analytics.com ...;<br /> connect-src 'self';<br /> style-src 'self' 'unsafe-inline' platform.twitter.com;<br /> child-src 'self' *.twitter.com noti.st;<br /> worker-src 'self';<br /> report-uri https://simonhearne.report-uri.com/a/d/g;<br /> report-to report-uri;'''</span><br /><br /><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">headers</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br /> <span class="token comment"># Override cache duration for assets with periods in the filename (i.e. static assets)</span><br /> <span class="token key property">for</span> <span class="token punctuation">=</span> <span class="token string">"/*.*"</span><br /> <span class="token punctuation">[</span><span class="token table class-name">headers.values</span><span class="token punctuation">]</span><br /> <span class="token key property">cache-control</span> <span class="token punctuation">=</span> <span class="token string">"public,max-age=360000"</span></code></pre>
<h2 id="configuring-headers-on-cloudflare" tabindex="-1">Configuring Headers on CloudFlare <a class="direct-link" href="https://simonhearne.com/2019/http-headers-fast-and-secure/#configuring-headers-on-cloudflare" aria-hidden="true">#</a></h2>
<p>If you are using CloudFlare in front of GitHub Pages for your static site, chances are that your caching is less than ideal. The default cache duration for static assets on GitHub Pages is five minutes, and pages are not cacheable. CDNs will generally be transparent, so these cache headers will not be changed.</p>
<p>CloudFlare configuration allows you to set a standard cache duration across all assets with a simple dropdown selection. While this makes it easy to improve cache durations, there is no granularity to cache some assets for different durations.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/dl-hvE9zyo-600.avif 600w, https://simonhearne.com/img/dl-hvE9zyo-900.avif 900w, https://simonhearne.com/img/dl-hvE9zyo-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/dl-hvE9zyo-600.webp 600w, https://simonhearne.com/img/dl-hvE9zyo-900.webp 900w, https://simonhearne.com/img/dl-hvE9zyo-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/dl-hvE9zyo-600.jpeg 600w, https://simonhearne.com/img/dl-hvE9zyo-900.jpeg 900w, https://simonhearne.com/img/dl-hvE9zyo-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Screenshot of CloudFlare UI to configure cache durations" loading="lazy" decoding="async" src="https://simonhearne.com/img/dl-hvE9zyo-600.jpeg" width="1200" height="563" /></picture><figcaption>Coarse cache control configuration in Cloudflare.</figcaption></figure>
<p>If you want more fine-grained control over headers, you will need to move your site away from GitHub pages or use CloudFlare workers to set custom headers. This is also required to set <code>Report-To</code> and <code>Content-Security-Policy</code> headers. Scott Helme has posted a recommended worker script to add important security headers: <a href="https://github.com/securityheaders/security-headers-cloudflare-worker/blob/master/worker.js">worker.js on GitHub</a>.</p>
Six Web Performance Technologies to Watch in 20202019-11-25T00:00:00Zhttps://simonhearne.com/2019/2020-predictions/<h2 id="introduction" tabindex="-1">Introduction <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#introduction" aria-hidden="true">#</a></h2>
<p>Reading the technical press you would be forgiven for thinking that 2020 is going to be a great year for web performance. Repeatedly touted are the blazing fast speeds we will achieve with 5G, the fundamental improvements that HTTP/3 will bring and how new devices will be faster than premium laptops.</p>
<p>I fear this outlook could make us lackadaisical about performance. 5G isn’t expected to reach even 20% of subscribers by 2025, HTTP/3 doesn’t change the speed of light nor the size of our JavaScript bundles, and ultra-fast devices will only reach the hands of the affluent few. In this article I'll look at some of the technologies which I believe could have a significant impact on web performance in 2020.</p>
<p>You can jump straight ahead to the sections on:</p>
<ol>
<li><a href="https://simonhearne.com/2019/2020-predictions/#1---jamstack">JAMstack</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#2---wasm-web-assembly">Web Assembly</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#3---edge-compute">Edge Compute</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#4---observability">Observability</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#5---platform-improvements">Browser Platform Improvements</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#6---web-monetisation">Web Monetisation</a></li>
<li><a href="https://simonhearne.com/2019/2020-predictions/#7---honourable-mentions">Honourable Mentions</a></li>
</ol>
<blockquote>
<p>Tl;dr: there will be no quick fixes. If we want a fast web in 2020 we have to take action now.</p>
</blockquote>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/ZlcGtBLY8r-600.avif 600w, https://simonhearne.com/img/ZlcGtBLY8r-900.avif 900w, https://simonhearne.com/img/ZlcGtBLY8r-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ZlcGtBLY8r-600.webp 600w, https://simonhearne.com/img/ZlcGtBLY8r-900.webp 900w, https://simonhearne.com/img/ZlcGtBLY8r-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ZlcGtBLY8r-600.jpeg 600w, https://simonhearne.com/img/ZlcGtBLY8r-900.jpeg 900w, https://simonhearne.com/img/ZlcGtBLY8r-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/ZlcGtBLY8r-600.jpeg" width="1200" height="925" /></picture><figcaption>Predicted 5G adoption rates.</figcaption></figure>
<p>5G will deploy slowly and will never replace 4G, that’s not the point of it. I believe we will see better traction with 5G home broadband than mobile connections. Not least because handling multiple gigabits per second of data will extract more juice from your battery than the technology of 2020 allows.</p>
<p>HTTP/3 will hopefully fix the <a href="https://github.com/andydavies/http2-prioritization-issues">flawed implementations</a> of stream prioritisation in HTTP/2. The implementation of the protocol is fundamentally different from H/2 (and H/1.x for that matter) but it still works within the same constraints. It will not rely on TCP’s implementation of congestion control - but we do still control, so it will be handled at the application layer instead. H/2 was hailed as a breakthrough for performance over H/1, until it was widely launched. For many sites it made little difference, for some it made a positive improvement and for some it made things worse. H/3 will likely show a similar outcome, with the actual gains defined by the architecture of individual sites.</p>
<p>Both of these technologies, 5G and H/3, sit at the network level - how the pipes are built and how to best get information across them. But in 2019 JavaScript has the single biggest impact on user experience. Shipping a 1MB JS application bundle to a mid-range Android phone 500ms faster will do little to help the time it takes to process the code. I say mid-range Android phone because that is the median device that web users have, Apple’s market share in terms of 2019 Q1 sales in the <a href="https://www.statista.com/statistics/620805/smartphone-sales-market-share-in-the-us-by-vendor/">US is 39%, with Samsung at 28%</a>, while the rest is a range of Android devices which sell for between $500 and $50 dollars.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/fDEua98f_4-600.avif 600w, https://simonhearne.com/img/fDEua98f_4-900.avif 900w, https://simonhearne.com/img/fDEua98f_4-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/fDEua98f_4-600.webp 600w, https://simonhearne.com/img/fDEua98f_4-900.webp 900w, https://simonhearne.com/img/fDEua98f_4-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/fDEua98f_4-600.jpeg 600w, https://simonhearne.com/img/fDEua98f_4-900.jpeg 900w, https://simonhearne.com/img/fDEua98f_4-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/fDEua98f_4-600.jpeg" width="1200" height="491" /></picture><figcaption>Digital platform usage by age - young people spend 72% of their time in apps.</figcaption></figure>
<p>We know that the average American changes their phone <a href="https://www.npd.com/wps/portal/npd/us/news/press-releases/2018/the-average-upgrade-cycle-of-a-smartphone-in-the-u-s--is-32-months---according-to-npd-connected-intelligence/">every 32 months</a> and worldwide the combined market share of premium brands Apple and Samsung is <a href="https://www.idc.com/promo/smartphone-market-share/vendor">just 33%</a>. The majority of users world-wide are on low- to mid-range Android phones, these consumers are far more likely to use a native app vs. the mobile web with perceived performance and data usage limits being the primary factors. The web is an open and democratic place, when people are forced to use only native apps they have a limited window on the web experience and the knowledge available therein.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/D6SxIqF6KO-600.avif 600w, https://simonhearne.com/img/D6SxIqF6KO-900.avif 900w, https://simonhearne.com/img/D6SxIqF6KO-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/D6SxIqF6KO-600.webp 600w, https://simonhearne.com/img/D6SxIqF6KO-900.webp 900w, https://simonhearne.com/img/D6SxIqF6KO-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/D6SxIqF6KO-600.jpeg 600w, https://simonhearne.com/img/D6SxIqF6KO-900.jpeg 900w, https://simonhearne.com/img/D6SxIqF6KO-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/D6SxIqF6KO-600.jpeg" width="1200" height="470" /></picture><figcaption>Predicted population sources for half of the next billion users.</figcaption></figure>
<p>There has been a fundamental shift over the past half-decade on the web: from thick servers and thin clients to thin servers and thick clients. The inexorable rise of frameworks such as Angular, React, Vue and their many cousins has been led by an assumption that managing state in the browser is quicker than a request to a server. This assumption, I can only assume, is made by developers who have flagship mobile devices or primarily work on desktop devices. We now find ourselves in a position where every website might bring with it a whole application framework, perhaps with hacks like server-side rendering and client-side rehydration to appease SEO crawlers. While this trend is unlikely to change course in 2020, we may see better web platform features to account for it.</p>
<p>There are a few exciting technologies which I think will be a positive change for web performance in 2020:</p>
<h2 id="1-jamstack" tabindex="-1">1 - JAMstack <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#1-jamstack" aria-hidden="true">#</a></h2>
<p>The JAM (JavaScript APIs Markup) stack is an <a href="https://jamstack.org/">architecture</a> for delivering fast sites, using static site generators. SSGs such as <a href="https://jekyllrb.com/">Jekyll</a>, <a href="https://gohugo.io/">Hugo</a> and <a href="https://www.11ty.io/">11ty</a> allow for a pleasant developer experience and rapid iteration, but compile to HTML - the native language of the web. These generators are currently widely used for personal blogs or small company sites due to their small footprint. Moves towards <a href="https://www.cmswire.com/web-cms/13-headless-cmss-to-put-on-your-radar/">headless CMS</a> solutions, client-side <a href="https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API">payment APIs</a> and cloud deployment could make SSGs viable for anything from news websites to ecommerce sites, effectively removing server processing time as a critical factor in performance and simplifying the process of deployment.</p>
<p><a href="https://www.netlify.com/">Netlify</a> has emerged as the market leader in workflow for the JAMstack. Netlify manages the whole workflow from development through to CDN deployment and augments the static site with a number of dynamic features. These features, such as identity management, forms and edge functions, broaden the scope of potential for JAMstack. There are also some great developer-friendly features such as deploying branches to unique URLs for testing, so there is no more need for a staging network. While Netlify is the clear leader in this space in 2019, I expect multiple solutions to appear in 2020 and build a competitive marketplace for enterprise JAMstack deployments. This has the potential to significantly erode the Wordpress marketplace, which currently accounts for <a href="https://www.whoishostingthis.com/compare/wordpress/stats/">34% of the web</a>!</p>
<p><strong>Who to follow:</strong> <a href="https://twitter.com/philhawksworth">@PhilHawksworth</a> & <a href="https://twitter.com/shortdiv">@shortdiv</a>.</p>
<h2 id="2-wasm-web-assembly" tabindex="-1">2 - WASM (Web Assembly) <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#2-wasm-web-assembly" aria-hidden="true">#</a></h2>
<figure align="center">
<p style="margin-bottom: -1.625em;" class="ciu_embed" data-feature="wasm" data-periods="future_1,current" data-accessible-colours="false">
<a href="http://caniuse.com/#feat=wasm"><picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/image/wasm.webp" />
<img src="https://caniuse.bitsofco.de/image/wasm.png" alt="Data on support for the wasm feature across the major browsers from caniuse.com" />
</picture></a></p>
<figcaption>WebAssembly has shipped to all major browsers.</figcaption>
</figure>
<p>WASM has come a long way in the past year and 2020 might see broader adoption. <a href="https://webassembly.org/">WASM</a> compiles in-browser 20 times faster than JavaScript, and WASM scripts will shortly be importable as modules into JS projects. The exciting possibility here is that the most processor intensive modules of popular frameworks, such as React’s Virtual DOM, can be re-written in a performant language such as Rust and compiled into WASM. In this example a single React release could raise the tide of web performance for all React-based websites. Until then, WASM provides a method to implement compute-intensive tasks in a language that performs much better than JavaScript (for <a href="https://webassembly.org/docs/use-cases/">applicable tasks</a>).</p>
<p><strong>Who to follow:</strong> <a href="https://twitter.com/patrickhamann">@PatrickHamann</a> & <a href="https://twitter.com/_jayphelps">@_JayPhelps</a>.</p>
<h2 id="3-edge-compute" tabindex="-1">3 - Edge Compute <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#3-edge-compute" aria-hidden="true">#</a></h2>
<p>Code executing at the CDN level allows us to offload processing from both client and server into the CDN platform. An existing implementation of WASM (or at least similar) is available on Fastly’s <a href="https://www.fastly.com/products/edge-compute">edge compute</a> platform. All major CDNs have now launched some form of edge compute service (see <a href="https://aws.amazon.com/lambda/">AWS' Lambda</a>, <a href="https://www.cloudflare.com/en-gb/products/cloudflare-workers/">CloudFlare Workers</a>, <a href="https://developer.akamai.com/akamai-edgeworkers-overview">Akamai EdgeWorkers</a>), interestingly all are built totally differently! Compute at the edge allows us to offload logic from both client and server, moving legacy server logic closer to the user, and intensive logic away to more controlled compute resources. Great applications of this are personalisation and image manipulation, both are tough to implement on the client but work well in server environments.</p>
<h2 id="4-observability" tabindex="-1">4 - Observability <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#4-observability" aria-hidden="true">#</a></h2>
<p>As the saying goes: "if you can't measure it, you can't manage it". 2019 has seen some significant improvements in observability: we have been moving away from purely objective measures (such as page load time) for a while now but this year we are seeing more visibility into how browsers process and render content. Metrics such as <a href="https://web.dev/cls/">Cumulative Layout Shift</a>, <a href="https://web.dev/tbt/">Total Blocking Time</a> and <a href="https://web.dev/lcp/">Largest Contentful Paint</a> aim to get closer to user experience measures. These simplify the process of measuring speed and put less dependence on developers to instrument their applications. <a href="https://httparchive.org/">HTTP Archive</a> and <a href="https://developers.google.com/web/tools/chrome-user-experience-report">Chome Real User Experience report (CrUX)</a> both provide a great comparative database of performance statistics, and <a href="https://github.com/GoogleChrome/lighthouse-ci">Lighthouse CI</a> provides an integration point to take all of this data and add good performance testing to build processes.</p>
<p>Google <a href="https://blog.chromium.org/2019/11/moving-towards-faster-web.html">has proposed</a> a "slow" badge in search results for websites. The stated goal is noble: "moving towards a faster web", but time and time again we have seen that the 'performance police' approach fails to affect meaningful change. Who knows what the workaround techniques will be: loading screens like 1999, screenshots rendered while the page loads in the background, UA sniffing to deliver a faster experience to googlebot.</p>
<figure class="no-shadow">
<img src="https://simonhearne.com/images/2020-predictions/chromeslowbadge.jpg" alt="Chrome screenshot showing a warning that 'this page usually loads slow'" loading="lazy" clickable="true" />
<figcaption>Chrome's proposed 'slow' badge</figcaption>
</figure>
<p>As a corollary to that statement: performance metrics for the slowest one-third of traffic saw improvements of up to 20% when Google <a href="https://webmasters.googleblog.com/2019/04/user-experience-improvements-with-page.html">included page speed as a ranking factor</a> in mobile search results. So it seems that imposing these perceived penalties does work, perhaps because technical SEO gets more budget than web performance?</p>
<blockquote>
<p>For the slowest one-third of traffic, we saw user-centric performance metrics improve by 15% to 20% in 2018. As a comparison, no improvement was seen in 2017.</p>
</blockquote>
<p>This approach of <a href="https://en.wikipedia.org/wiki/Carrot_and_stick">stick vs. carrot</a> is a bit of a poke in the eye, especially considering that a huge hole in performance observability comes from Google Analytics. GA is the <a href="https://almanac.httparchive.org/en/2019/third-parties#providers">most widely used</a> third-party tag on the planet, collecting data from the majority of the sites you visit around the web. GA provides 'Site Speed' reports, which should be a good thing, yet out of the box GA only supports the most basic of legacy performance measures. These are randomly sampled from 1% of pageviews with no ability to correlate performance with business outcomes. Worse still, the only aggregate statistic available is the arithmetic mean, which is known to be <a href="http://highscalability.com/blog/2012/5/23/averages-web-performance-data-and-how-your-analytics-product.html">nearly useless</a> in web performance measures.</p>
<figure class="no-shadow">
<img src="https://simonhearne.com/images/2020-predictions/ga_timers.png" alt="Google Analytics screenshot showing the list of timers: dom content loaded, dom interactive and page load time" maxwidth="400" loading="lazy" clickable="true" />
<figcaption>Google Analytics default timers are very limited</figcaption>
</figure>
<p>There is still a long way to go to improve observability. As an industry we could instantly improve observability for at least <a href="https://trends.builtwith.com/analytics/Google-Analytics">thirty million</a> websites by simply improving the default performance data presented in Google Analytics.</p>
<p>It is <em>critical</em> to measure site speed using a Real User Measurement solution, otherwise you have zero visibility of how your users perceive the performance of your application (or worse, metrics that point you in the wrong direction). For the most simple setup, go for <a href="https://speedcurve.com/features/lux/">SpeedCurve Lux</a>. For the most flexibility and an enterprise solution, go for <a href="https://www.akamai.com/uk/en/products/performance/mpulse-real-user-monitoring.jsp">Akamai mPulse</a>. If you already have an Application Performance Measurement (APM) solution they may have a plugin to measure real user performance, such as <a href="https://www.dynatrace.com/support/doc/appmon/user-experience-management/">Dynatrace UEM</a> and <a href="https://newrelic.com/products/browser-monitoring">New Relic Browser</a>. Whatever solution you choose, make sure that you can measure what is important to your users and your product, and correlate speed with business success.</p>
<p>I can only hope that the web will get faster as metrics improve, RUM becomes more prevalent and performance statistics become part of executive reports. Slow sites cost money, and observability helps us to allocate funds to improve performance.</p>
<p><strong>Who to follow:</strong> <a href="https://twitter.com/anniesullie">@anniesullie</a> & <a href="https://twitter.com/eanakashima">@eanakashima</a>.</p>
<h2 id="5-platform-improvements" tabindex="-1">5 - Platform improvements <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#5-platform-improvements" aria-hidden="true">#</a></h2>
<p>A lot of improvements are coming from browsers like Chrome. It has offered <a href="https://support.google.com/chrome/answer/2392284?co=GENIE.Platform%3DAndroid&hl=en-GB">Data Saver mode</a> since 2013, which has very high opt-in rates for low-end devices. This mode in most cases presents a hint to web servers to <a href="https://calendar.perfplanet.com/2018/data-shaver-strategies/">deliver a lighter page</a> (which could be handled at the edge to deliver a fast, light experience). Chrome also <a href="https://web.dev/native-lazy-loading/">released</a> the <code>loading</code> attribute in 2019 - a hint as to the loading priority of resources. Combining these two, Chrome will by default <a href="https://web.dev/native-lazy-loading/#load-in-distance-threshold">lazy load offscreen images</a> for users on poor connection or if data saver is enabled. This simple change to the web platform should have a significant impact on web performance, without the intervention of web developers. While this may feel intrusive, browsers have long enforced certain policies for reasons of privacy. Adding performance as a signal in these decisions should mean a more equitable web for all users.</p>
<figure class="no-shadow">
<p style="margin-bottom: -1.625em;" class="ciu_embed" data-feature="loading-lazy-attr" data-periods="future_1,current" data-accessible-colours="false">
<a href="http://caniuse.com/#feat=loading-lazy-attr"><picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/image/loading-lazy-attr.webp" />
<img src="https://caniuse.bitsofco.de/image/loading-lazy-attr.png" alt="Data on support for the loading-lazy-attr feature across the major browsers from caniuse.com" />
</picture></a></p>
<figcaption>Loading attribute support is limited to Chromium-based browsers.</figcaption>
</figure>
<p>Here's hoping that these changes driven by the Chrome team are standardised and make it to other browsers. This simple change could have a drastic effect on user experiences for those on the worst network connections and those with limited or metered data connections.</p>
<p><strong>Who to follow:</strong> <a href="https://twitter.com/katiehempenius">@KatieHempenius</a> & <a href="https://twitter.com/addyosmani">@AddyOsmani</a>.</p>
<h2 id="6-web-monetisation" tabindex="-1">6 - Web monetisation <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#6-web-monetisation" aria-hidden="true">#</a></h2>
<p>Much of the <em>slowness</em> of the web comes from our attempts to monetise visitors. From advertising and tracking to personalisation and live chat, these technologies all get in the way of the user experience in order to generate revenue. The business model on the web is not as clear cut as it is on the high street or in the newsagent, and we are still struggling to reconcile the user expectation of a free and open web with the business requirement to generate revenue.</p>
<p>Improving monetisation through the web using built-in APIs such as the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API">Payment Request API</a>, <a href="https://webmonetization.org/">Web Monetization API</a> and even <a href="https://brave.com/brave-rewards/">Brave Rewards</a> should make it easier to monetize content without the need for intrusive third-party content. This change could see a drastic improvement in experience for the web users of 2020, especially on news and magazine websites.</p>
<p>People <a href="https://wpostats.com/tags/engagement/">spend longer on fast websites</a>, so this could be one of the elusive win-wins of web development.</p>
<p><strong>Who to follow:</strong> <a href="https://twitter.com/Coil">@Coil</a> & <a href="https://twitter.com/Interledger">@Interledger</a>.</p>
<h2 id="7-honourable-mentions" tabindex="-1">7 - Honourable mentions <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#7-honourable-mentions" aria-hidden="true">#</a></h2>
<p>Deliberately missing from these predictions are <a href="https://moz.com/blog/accelerated-mobile-pages-whiteboard-friday">Google's AMP</a>, <a href="https://instantarticles.fb.com/">Facebook Instant Articles</a>, <a href="https://developers.google.com/web/fundamentals/primers/service-workers">Service Workers</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">Web Workers</a>, <a href="https://www.smashingmagazine.com/2016/08/a-beginners-guide-to-progressive-web-apps/">Progressive Web Apps</a>, <a href="https://webvr.info/">Web Virtual Reality</a> and AI. I don't think that these technologies are critical for shifting the needle on the median person's web experience, for a range of political and technical reasons. The closest on the list is Progressive Web Apps, as they may be the key to closing the gap between the web and native apps. Samsung recently <a href="https://medium.com/samsung-internet-dev/introducing-progressive-web-apps-to-samsung-galaxy-store-47ecd317725b">announced support</a> for including PWAs in their US App Store, although it does require you to email them your URL and sign some additional licenses. Google has an <a href="https://medium.com/@firt/google-play-store-now-open-for-progressive-web-apps-ec6f3c6ff3cc">odd workaround</a> where you can ship your website as an app in the Play Store, but the process is far from straightforward. Apple, unsurprisingly, is steadfastly protecting its App Store revenue (which is <a href="https://www.theverge.com/2019/3/20/18273179/apple-icloud-itunes-app-store-music-services-businesses">greater than its revenue from device sales</a>) by barely entertaining the idea, although Safari does <a href="https://medium.com/@firt/whats-new-on-ios-12-2-for-progressive-web-apps-75c348f8e945">support some of the critical features</a>. Unfortunately I don't think we will see a market shift in this respect in 2020.</p>
<p>There are, however, changes afoot which might mean a significant improvement to web UX in the EU. It looks like EU regulators have accepted that existing GDPR regulations have led to a web experience laden with oft-ignored (or ignorantly accepted) GDPR notices. These have become a blight on the web and do little to protect users - the average consent form takes 42 seconds to opt-out of non-essential cookies. If this consent management moves to the browser, a single native UI could be the only requirement for users to manage their consent, improving default security posture as well as the web experience. This is one of the options being considered for <a href="https://en.wikipedia.org/wiki/EPrivacy_Regulation_(European_Union)">ePrivacy</a> - a proposal for better regulations in the EU. At the speed of government though, I'm not sure we will see change formalised in 2020, let alone adopted and regulated.</p>
<blockquote>
<p>The cookie provision, which has resulted in an overload of consent requests for internet users, will be streamlined. The new rule will be more user-friendly, as browser settings will provide for an easy way to accept or refuse tracking cookies and other identifiers. The proposal also clarifies that no consent is needed for non-privacy-intrusive cookies improving internet experience (like to remember shopping cart history) or cookies used by a website to count the number of visitors.</p>
</blockquote>
<p>Explicit opt-out, or preferences managed in the browser, could dramatically improve the performance on many sites. Hello magazine is an example where over 11MB of content is loaded on the mobile first view, a significant amount of which is third-party. The opt-in to this is explicit: "By browsing this site you are agreeing to this", and the link to manage your preferences is inaccessible once you interact with the site.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/2020-predictions/hello.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/t6CT84qv8S-600.avif 600w, https://simonhearne.com/img/t6CT84qv8S-900.avif 900w, https://simonhearne.com/img/t6CT84qv8S-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/t6CT84qv8S-600.webp 600w, https://simonhearne.com/img/t6CT84qv8S-900.webp 900w, https://simonhearne.com/img/t6CT84qv8S-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/t6CT84qv8S-600.jpeg 600w, https://simonhearne.com/img/t6CT84qv8S-900.jpeg 900w, https://simonhearne.com/img/t6CT84qv8S-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Hello magazine with explicit GDPR opt-in loading 471 requests" loading="lazy" decoding="async" src="https://simonhearne.com/img/t6CT84qv8S-600.jpeg" width="1200" height="1117" /></picture></a><figcaption>Hello magazine has explicit opt-in, opt-out is not possible once you scroll the page.</figcaption></figure>
<p>If you are looking at the future of web performance, the most important thing you can do is to make your experiences fast now. The future is not bringing any silver bullets, which means it is more important than ever to measure and optimise your users' experience.</p>
<h2 id="further-reading" tabindex="-1">Further Reading <a class="direct-link" href="https://simonhearne.com/2019/2020-predictions/#further-reading" aria-hidden="true">#</a></h2>
<ul>
<li><a href="https://blog.superhuman.com/performance-metrics-for-blazingly-fast-web-apps-ec12efa26bcb">Performance Metrics for Blazingly Fast Web Apps</a></li>
<li><a href="https://www.filamentgroup.com/lab/5g/">5G Will Definitely Make the Web Slower, Maybe</a></li>
<li><a href="https://www.youtube.com/watch?v=iaWLXf1FgI0">Speed tooling evolutions: 2019 and beyond [video:23min]</a></li>
<li><a href="https://www.gsmaintelligence.com/research/?file=c5f35990dcc742733028de6361ccdf3b&download">GSMA Intelligence: Global Mobile Trends 2020 [pdf:3.5MB]</a></li>
<li><a href="https://www.gsmaintelligence.com/research/?file=b9a6e6202ee1d5f787cfebb95d3639c5&download">GSMA Intelligence: The Mobile Economy 2019 [pdf:3.3MB]</a></li>
<li><a href="https://www.comscore.com/Insights/Presentations-and-Whitepapers/2019/Global-State-of-Mobile">Comscore Global State of Mobile 2019 Whitepaper [registration required]</a></li>
<li><a href="https://perfnow.nl/speakers">All of the talks from Performance.Now() conference 2019</a> :)</li>
</ul>
<script>!function(){document.addEventListener("DOMContentLoaded",function(){for(var e=document.getElementsByClassName("ciu_embed"),t=0;t<e.length;t++){var a=e[t],i=a.getAttribute("data-feature"),n=a.getAttribute("data-periods"),s=a.getAttribute("data-accessible-colours")||"false",r=a.getAttribute("data-image-base")||"none";if(i){var o="https://caniuse.bitsofco.de/embed/index.html",d='<iframe src="'+o+"?feat="+i+"&periods="+n+"&accessible-colours="+s+"&image-base="+r+'" frameborder="0" width="100%" height="400px"></iframe>';a.innerHTML=d}else a.innerHTML="A feature was not included. Go to <a href='https://caniuse.bitsofco.de/#how-to-use'>https://caniuse.bitsofco.de/#how-to-use</a> to generate an embed."}var c=window.addEventListener?"addEventListener":"attachEvent";(0,window[c])("attachEvent"==c?"onmessage":"message",function(t){var a=t.data;if("string"==typeof a&&a.indexOf("ciu_embed")>-1)for(var i=a.split(":")[1],n=a.split(":")[2],s=0;s<e.length;s++){var r=e[s];if(r.getAttribute("data-feature")===i){var o=parseInt(n)+30;r.childNodes[0].height=o+"px";break}}},!1)})}();</script>
Deep dive into third-party performance2019-11-22T00:00:00Zhttps://simonhearne.com/2019/third-party-deep/<p><lite-youtube videoid="uXv9JFvrnwo" playlabel="Play: Deep dive into third-party performance"></lite-youtube></p>
<p><a class="link-button no-link" href="https://simonhearne.com/presentations/third-party-deep/#/">View Slides</a></p>
Native image lazy loading has landed in Chrome, maybe don't use it2019-08-13T00:00:00Zhttps://simonhearne.com/2019/native-lazy-loading-in-chrome/<h2 id="lazy-loading-images" tabindex="-1">Lazy Loading Images <a class="direct-link" href="https://simonhearne.com/2019/native-lazy-loading-in-chrome/#lazy-loading-images" aria-hidden="true">#</a></h2>
<p>Web browsers are complex pieces of software, but still treat images relatively naïvely. See, for example, a section of this waterfall where almost 100 images are loaded immediately on first-view:</p>
<figure align="center">
<img src="https://simonhearne.com/images/waterfall_images.png" alt="Waterfall section showing bandwidth consumed by off-screen images delays javascript resources" loading="lazy" />
<figcaption>Waterfall section showing bandwidth consumed by off-screen images</figcaption>
</figure>
<p>On this client's site all but four of the images downloading in the screenshot (shown in purple) are off-screen. Those images are totally invisble to the user on desktop and mobile, but compete for bandwidth with critical resources like the application JavaScript (yellow). Making matters even worse - browsers will download images in the order that they appear in the HTML document, and this client has a dozen images in their hero navigation menu which is at the very top of the HTML. Again, these are invisible to the user at page load time, but are loaded before the critical hero images.</p>
<p>Lazy loading is an approach to provide a hint (or trick) the browser so that visible images are prioritised higher than offscreen images. There have been multiple libraries which offer this functionality, see <a href="https://github.com/aFarkas/lazysizes">lazysizes.js</a>. The general premise is to remove the <code>src</code> attribute, so the browser does not download anything, then add a <code>src</code> later in the page load which will trigger an immediate download by the browser.</p>
<h2 id="native-lazy-loading" tabindex="-1">Native lazy loading <a class="direct-link" href="https://simonhearne.com/2019/native-lazy-loading-in-chrome/#native-lazy-loading" aria-hidden="true">#</a></h2>
<p>As of <a href="https://www.chromestatus.com/feature/5645767347798016">Chrome 76</a>, released Jul 30 2019, web developers can provide a native hint to the browser that an image can be loaded lazily by adding the attribute <code>loading='lazy'</code> to the <code>img</code> element. See more details on the <a href="https://web.dev/native-lazy-loading">web.dev</a> blog.</p>
<h2 id="not-ready-for-prime-time" tabindex="-1">Not ready for prime time <a class="direct-link" href="https://simonhearne.com/2019/native-lazy-loading-in-chrome/#not-ready-for-prime-time" aria-hidden="true">#</a></h2>
<p>This release has received a lot of attention on developer twitter. Multiple tweets show developers removing their 'legacy' lazy loading approaches and replacing them with the simple <code>loading</code> attribute. While adding the <code>loading</code> attribute is a positive step for sites without any prior lazy loading strategy, <strong>moving to it as a replacement will almost definitely degrade overall user experience</strong>.</p>
<p>The browser support for <code>loading</code> is <a href="https://caniuse.com/#feat=loading-lazy-attr">less than 1%</a> at the time of writing. This number will climb to about 60% globally once the Chrome 76 rollout to mobile and desktop completes. There will be a further ~1% increase in global support when MS Edge updates.</p>
<figure align="center">
<img src="https://simonhearne.com/images/lazy_caniuse.png" alt="CanIUse.com for native lazy loading on Aug 13 2019 shows 0.15% support globally" loading="lazy" />
<figcaption>CanIUse.com for native lazy loading on Aug 13 2019 (<a href="https://caniuse.com/#feat=loading-lazy-attr">see latest stats</a>)</figcaption>
</figure>
<p>Safari is notably lacking from the support list. This is not a surprise, as Apple are notoriously slow to update their browser (and are opaque about roadmap), but is a serious concern for most western websites. If your Safari traffic is greater than 50%, like many of my publishing and fashion clients, moving to the <code>loading</code> attribute will result in a net-negative performance change.</p>
<h2 id="the-future-of-lazy-loading" tabindex="-1">The future of lazy loading <a class="direct-link" href="https://simonhearne.com/2019/native-lazy-loading-in-chrome/#the-future-of-lazy-loading" aria-hidden="true">#</a></h2>
<p>The initial implementation of <code>loading</code> in Chrome is not complete. There are <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=875403&q=component%3ABlink%3ELoader%3ELazyLoad%20print&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified">issues with printing and exporting</a> (looks to be resolved in v77) and the options <code>eager</code> and <code>auto</code> are currently <a href="https://chromestatus.com/feature/5645767347798016">identical in functionality</a>. In the future, the plan is for <code>auto</code> (the default behaviour if no <code>loading</code> attribute is supplied) to be determined by the user's network conditions or data saver preference (or is it <a href="https://timkadlec.com/remembers/2019-03-14-making-sense-of-chrome-lite-pages/">Lite Pages</a>?). This means that the performance of your pages should be improved for users with the worst experience, but outside of your control (unless you add the attribute and set it to <code>eager</code> or <code>lazy</code>).</p>
<p>If you're not currently <a href="https://calendar.perfplanet.com/2018/data-shaver-strategies/">collecting the data saver preference</a> in your analytics tool of choice, this could result in some confusing metrics!</p>
<h2 id="conclusions" tabindex="-1">Conclusions <a class="direct-link" href="https://simonhearne.com/2019/native-lazy-loading-in-chrome/#conclusions" aria-hidden="true">#</a></h2>
<p>Supporting lazy loading hints natively in the browser is a huge step forward for web performance. Using the <code>loading</code> attribute as the sole approach for lazy loading will be the recommendation once WebKit and Safari implement support. For now, though, it should only be used as an enhancement and not as a replacement for existing strategies.</p>
<p>A great worked example was <a href="https://medium.com/bbc-design-engineering/native-lazy-loading-has-arrived-c37a165d70a5">published by the BBC</a>, where they added the <code>loading</code> attribute to images in an internal app, presumably a very high proportion of Chrome Desktop traffic. The results were a 50% reduction in load time and 40 fewer requests per pageview. This is a great use case for native lazy loading.</p>
<p>In the interests of full disclosure, I use the <code>loading</code> attribute on this site (try inspecting the images in this post). I had no lazy loading strategy beforehand, so it is a progressive enhancement. Over 70% of the traffic to this site is from Chrome, so this should be a reasonable improvement. I had previously experimented with a home-brewed lazy loading technique, but the JavaScript overhead was significant enough to negate most performance benefits.</p>
WebPageTest Private - 'no successful results'2019-08-09T00:00:00Zhttps://simonhearne.com/2019/wpt-private-no-successful-results/<p>I recently configured a new <a href="https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Private%20Instances/README.md">private WebPageTest instance</a> on AWS to use SSL provided by <a href="https://letsencrypt.org/">Let's Encrypt</a>, using EFF's awesome <a href="https://certbot.eff.org/">CertBot</a>.</p>
<p>When running my first set of tests I noticed big delays (multiple minutes per test) and when the tests finally completed they all reported <code>The test completed but there were no successful results.</code>.</p>
<p>A quick look at the access log showed that the agents were getting jobs, but not submitting the results back to the server. I saw a bunch of <code>HTTP 301</code> responses to the workdone POSTs:</p>
<p><code>POST /work/workdone.php?run=1... HTTP/1.1" 301 178 "-" "python-requests/2.22.0"</code></p>
<p>Seeing the <code>301</code> responses (and no subsequent <code>200</code>s) made me realise that CertBot had added a default redirect from port 80 to port 443 to my nginx config (found at <code>/etc/nginx/sites-enabled/default</code> for the WebPageTest AMI).</p>
<pre class="language-js"><code class="language-js">server <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>$host <span class="token operator">=</span> <span class="token keyword">private</span><span class="token punctuation">.</span>wpt<span class="token punctuation">.</span>host<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token number">301</span> <span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>$host$request_uri<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> # managed by Certbot<br /> listen <span class="token number">80</span><span class="token punctuation">;</span><br /> server_name <span class="token keyword">private</span><span class="token punctuation">.</span>wpt<span class="token punctuation">.</span>host<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token number">404</span><span class="token punctuation">;</span> # managed by Certbot<br /><span class="token punctuation">}</span></code></pre>
<p>Simply commenting out this <code>server</code> block to remove the redirect and bouncing the nginx server immediately resolved the issue. CertBot does ask if it should add the redirect, so simply saying no during configuration would have solved the issue! It would be trivial to allow test agents to communicate on :80 based on the workdone path to get the best of both worlds.</p>
<p>If that is not the cause of your issue, it may be caused by the use of 64-bit windows clients which cannot do traffic shaping using dummynet. Simply adding <code>connectivity=LAN</code> to <code>locations.ini</code> for the test locations should resolve the issue.</p>
Why Site Speed is Critical for Online Publishers2019-08-07T00:00:00Zhttps://simonhearne.com/2019/site-speed-for-publishers/<p>It has been well documented that site speed is a major factor in business success for e-commerce websites. If you can demonstrate that improving page load times by 500ms increases revenue by 5% there is a clear return on investment to make the speed improvements.</p>
<p>Digital publishing has not had so much documented evidence for the benefits for site speed. There are multiple factors behind this, based on the business model of the site. Subscription-based publishers have a captive audience - if you've already paid to access content, surely an article loading a second slower won't stop you reading the content? For ad-driven publishers there is a conflict between AdOps teams piling on tens or hundreds of third-party tags (each promising a slice of ad revenue) and the development team trying to ship editorial content.</p>
<blockquote>
<p>Optimizely added artificial latency to the Telegraph and saw page views plummet: by 11% for a 4 second delay and 44% for a 20 second delay.</p>
</blockquote>
<p class="align-right pull-up small"><a href="https://wpostats.com/2016/07/04/optimizely-pageviews.html">source</a></p>
<h2 id="subscription-based-publishers" tabindex="-1">Subscription-based Publishers <a class="direct-link" href="https://simonhearne.com/2019/site-speed-for-publishers/#subscription-based-publishers" aria-hidden="true">#</a></h2>
<p>There are two angles to look at site speed for subscription-based publishers: acquisition and retention. When acquiring new subscribers, these sites can be viewed very much like an ecommerce player - you're trying to convert users from paying nothing to purchasing a subscription.</p>
<p>The e-commerce model does not work when it comes to retention, however. Once a user has purchased a subscription they will have a choice of whether to renew or not. That decision will be based at least partly on their experience of the product over the previous period, there will be a calculation of the value of the content they consume versus the ongoing cost of subscription.</p>
<p>As the FT found out, slower pages result in fewer articles viewed. The drop in articles gets worse over time, presumably as subscribers get increasingly frustrated.</p>
<blockquote>
<p>Financial Times added a one second delay to every page view and saw a 4.9% drop in the number of articles users read over a 7 day window. A two-second delay resulted in a 4.4% drop, and a three second delay saw a 7.2% drop. After twenty-eight days the two and three second variants both resulted in further drops in engagement.</p>
</blockquote>
<p class="align-right pull-up small"><a href="https://wpostats.com/2016/04/13/ft-page-view.html">source</a></p>
<p>Whilst the FT study clearly showed a direct correlation between speed and articles consumed, they did not continue to measure the effect on subscription renewal. It is fair to assume, though, that subscribers who have a poor experience (and thus consume less content) are less likely to renew their subscription. You may be missing out on critical business data if you're not tracking subscriber views, performance and attrition.</p>
<blockquote>
<p>GQ cut load time by 80% and saw an 80% increase in traffic. Median time spent on the site also increased by 32%.</p>
</blockquote>
<p class="align-right pull-up small"><a href="https://digiday.com/media/gq-com-cut-page-load-time-80-percent/">source</a></p>
<p>The interactive chart below shows the relationship between load time and the likelihood that a user will bounce or continue to a different page (retention) for a range of publishing websites.</p>
<div class="vega-chart loading" id="vis1" data-spec="/data/retention.json">Loading chart...</div>
<h2 id="ad-driven-publishers" tabindex="-1">Ad-driven Publishers <a class="direct-link" href="https://simonhearne.com/2019/site-speed-for-publishers/#ad-driven-publishers" aria-hidden="true">#</a></h2>
<p>The standard e-commerce model again does not work for publishers who make the majority of their revenue from advertising. Success is measured by ads viewed and interactions. There are a number of ways that performance impacts these key metrics:</p>
<ul>
<li>Slow pages lead to higher bounce rates</li>
<li>Slow pages lead to lower interaction rates</li>
<li>Slow pages lead to <a href="https://webmasters.googleblog.com/2010/04/using-site-speed-in-web-search-ranking.html">poorer organic SEO rankings</a></li>
<li>Slow pages lead to <a href="https://www.synapsesem.com/slow-site-load-times-crippling-digital-marketing-programs/">higher ad costs</a></li>
</ul>
<p>At the most basic level, we know that faster pages lead to an increase in page views. This increase comes from a number of factors: reducing bounce rate (is this page ever going to load?), increasing session length (I've only got a minute to catch up on the news) and favourable rankings from search engines which include speed as a ranking factor. Assuming that there is a near-linear relationship between page views and ad revenue, there is an obvious benefit to improving site speed.</p>
<blockquote>
<p>Tokopedia reduced render time from 14s to 2s for 3G connections and saw a 19% increase in visitors, 35% increase in total sessions, 7% increase in new users, 17% increase in active users and 16% increase in sessions per user.</p>
</blockquote>
<p class="align-right pull-up small"><a href="https://wpostats.com/2018/05/30/tokopedia-new-users.html">source</a></p>
<p>The interactive chart below shows the relationship between first-page load time and session length (pages viewed per session) for a range of publishing websites. Session length can drop by over 60% when comparing the fastest page loads with the slowest!</p>
<div class="vega-chart loading" id="vis2" data-spec="/data/session_length.json">Loading chart...</div>
<h3 id="sponsored-content-takeovers-and-advertorials" tabindex="-1">Sponsored content, takeovers and advertorials <a class="direct-link" href="https://simonhearne.com/2019/site-speed-for-publishers/#sponsored-content-takeovers-and-advertorials" aria-hidden="true">#</a></h3>
<p>The publishing industry is looking to monetise their visits in novel ways as ad & content blockers become more prevalent. Full page takeovers fill otherwise unused whitespace on the page with sponsored promotional material. The commercial arrangement is similar to sponsored content and is typically based on the duration of the takeover or a goal number of pageviews. Increasing pageviews means either higher value for a duration-based takeover, or a shorter duration for a pageview-based takeover. Either way, this could mean more revenue for the publisher. On a secondary note, takeover content <em>should</em> load after the main content of the page. Making the page faster will mean that the promotional content will be loaded sooner and thus seen by more visitors.</p>
<h2 id="improving-speed" tabindex="-1">Improving Speed <a class="direct-link" href="https://simonhearne.com/2019/site-speed-for-publishers/#improving-speed" aria-hidden="true">#</a></h2>
<p>Site speed should be measured using metrics which are closest to user experience. Time to interactive & first meaningful paint are generally the best metrics to use for publishing sites where third-party content may significantly skew the load event. Some key methods to improve time to interactive and paint timing metrics are to prioritise critical content:</p>
<ol>
<li><a href="https://medium.com/the-telegraph-engineering/improving-third-party-web-performance-at-the-telegraph-a0a1000be5#4123">Defer all JavaScript</a></li>
<li><a href="https://hpbn.co/building-blocks-of-tcp/#slow-start">Keep HTML documents < 14kB to maximise the first round-trip</a></li>
<li><a href="https://www.searchenginejournal.com/lazy-loading-attribute/302155/">Use the <code>loading='lazy'</code> attribute (cautiously) to defer non-critical iframes and images</a></li>
<li><a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization#preload_your_webfont_resources">Use <code>preload</code> for critical fonts</a></li>
<li><a href="https://developers.google.com/web/updates/2016/02/font-display">Use <code>font-display: swap</code> to improve render times</a></li>
<li><a href="https://simonhearne.com/2015/find-third-party-assets/">Keep track of third-party tags</a></li>
<li><a href="https://medium.com/the-telegraph-engineering/improving-third-party-web-performance-at-the-telegraph-a0a1000be5">Remove unnecessary and unused third-party tags</a></li>
</ol>
<hr />
<p>If you would like advice on measuring, improving and advocating for site speed, please <a href="mailto:simon@hearne.me">get in touch</a>!</p>
Lead Developer Conference (London) - Day 1!2018-06-27T00:00:00Zhttps://simonhearne.com/2018/leaddev-day-1/<h2 id="alice-goldfuss-the-container-operator-s-manual" tabindex="-1">Alice Goldfuss - The Container Operator’s Manual <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#alice-goldfuss-the-container-operator-s-manual" aria-hidden="true">#</a></h2>
<p>Containers are generally presented in theory, Alice tells us how they work in real-life. Alice is an SRE at GitHub and has run Docker in production for 3.5 years, so knows a bit about containers in the wild!</p>
<ol>
<li>Containers have strengths.</li>
</ol>
<ul>
<li>Stateless applications / processes</li>
</ul>
<ol start="2">
<li>Containers have weaknesses.</li>
</ol>
<ul>
<li>Stateful applications / databases
Don't containerise databases - use cloud services</li>
</ul>
<ol start="3">
<li>Containers need friends.</li>
</ol>
<ul>
<li>Multiple tools, vendors and products required to manage, deploy, network, monitor etc.</li>
</ul>
<ol start="4">
<li>Containers need headcount</li>
</ol>
<ul>
<li>You need a whole team of engineers (6-8 people ideally) to build a container platform:</li>
<li>operations</li>
<li>deployments</li>
<li>tooling</li>
<li>monitoring</li>
<li>kernel</li>
<li>network</li>
<li>infosec</li>
<li>internal adoption</li>
<li>project manager</li>
</ul>
<blockquote>
<p>Do you want containers, or a blog post?!</p>
</blockquote>
<h2 id="dan-persa-first-steps-as-a-lead-talk" tabindex="-1">Dan Persa - First Steps as a Lead (⚡️ talk) <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#dan-persa-first-steps-as-a-lead-talk" aria-hidden="true">#</a></h2>
<p>@danpersa</p>
<p>Dan is an engineering lead at Zalando, based in Berlin.</p>
<blockquote>
<p>Dealing with people problems is much more interesting than dealing with development problems</p>
</blockquote>
<p>The responsibility of tech leads varies by company, from a technical lead role to a line management role. It is important to understand the role at your company and whether it fits your career goals.</p>
<p>Dan had three months of intense mentoring from an experienced tech lead at Zalando as part of a transition process from engineer to lead.</p>
<p>Dan's tips on how to learn fast:</p>
<ol>
<li>get a mentor</li>
<li><em>apply</em> what you read in books</li>
<li>reflect on your past - how you were managed</li>
</ol>
<p>Trust instead of control:</p>
<ol>
<li>earn the respect of the team through pair-programming</li>
<li>regular 1:1s</li>
<li>transparency around performance evaluations</li>
</ol>
<p>The lead's role is to help the team discover solutions to challenges, not to solve them yourself.</p>
<p>Not taking a decision is a decision in itself.</p>
<p>Give feedback as soon as possible, with a balance of positive and constructive feedback. Always ask for feedback in return.</p>
<h2 id="alexandra-hill-the-art-of-giving-and-receiving-code-reviews-gracefully-talk" tabindex="-1">Alexandra Hill - The art of giving and receiving code reviews gracefully (⚡️ talk) <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#alexandra-hill-the-art-of-giving-and-receiving-code-reviews-gracefully-talk" aria-hidden="true">#</a></h2>
<p>Code reviews are a key factor in improving:</p>
<ul>
<li>functionality</li>
<li>readability</li>
<li>meaintainability</li>
<li>scalability</li>
</ul>
<p>But, they can be a big cause of conflict.</p>
<p>Low impact issues can be automated away.</p>
<p>Dual concerns model for conflict resolution.</p>
<p>Solutions to improve code reviews:</p>
<ol>
<li>Pair program</li>
<li>Discsuss tasks prior to implementation</li>
<li>Don't silo codebases</li>
<li>Ensure that everyone reviews and everyone is reviewed</li>
</ol>
<p>Reviewers can raise by a grade or two.</p>
<p>Use 'we' instead of 'you'. Ask questions instead of making statements. Don't make demands of people or use challenging language.</p>
<p>Don't forget to give positive feedback.</p>
<h2 id="pia-nilsson-growing-teams-to-continuously-deliver" tabindex="-1">Pia Nilsson - Growing teams to continuously deliver <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#pia-nilsson-growing-teams-to-continuously-deliver" aria-hidden="true">#</a></h2>
<p>@pia_nilsson</p>
<p>Increasing flow in the back-end continuous delivery squad:</p>
<ol>
<li>WIP limits to increase focus, but also increased stress as engineers focussed on finishing tasks, even when delays were out of their control.</li>
<li>TDD & DDD</li>
<li>Pairing & Mob programming</li>
</ol>
<p>"Yes, and" - never say "No" or "but".</p>
<p>Non-violent communication method.</p>
<p>Objectives and key results (OKR).</p>
<p>Despicable design. Define the worst possible experience.</p>
<h2 id="menno-van-slooten-how-i-learned-to-stop-worrying-and-love-meetings" tabindex="-1">Menno van Slooten - How I learned to stop worrying and love meetings <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#menno-van-slooten-how-i-learned-to-stop-worrying-and-love-meetings" aria-hidden="true">#</a></h2>
<p>@mennovanslooten</p>
<blockquote>
<p>Becoming a lead: the subtle art of not making a difference.</p>
</blockquote>
<ol>
<li>Let go of programming - chasing your old job prevents you from seeing the value in what you <em>are</em> doing</li>
<li>If you attend a meeting your developers don't have to, so they can focus on what they love to do</li>
<li>Learn to love interruptions - they are opportunities to help people</li>
</ol>
<p>How do you say no??</p>
<h2 id="jenny-duckett-building-sustainable-teams-to-handle-uncertainty" tabindex="-1">Jenny Duckett - Building sustainable teams to handle uncertainty <a class="direct-link" href="https://simonhearne.com/2018/leaddev-day-1/#jenny-duckett-building-sustainable-teams-to-handle-uncertainty" aria-hidden="true">#</a></h2>
<p>@jenny_duckett</p>
<p>Have a single clear goal for your team, reiterate it constantly. Use it to add context to tasks.</p>
<p><a href="https://gdstechnology.blog.gov.uk/2016/09/08/our-top-12-mob-programming-tips-and-thoughts/">Mobbing</a></p>
<p>Use every task as an opportunity for the developer to grow & learn.</p>
<blockquote>
<p>Make yourself dispensable</p>
</blockquote>
<p>Don't insulate your team too much, make it clear where their work fits in the wider picture.</p>
Delta V Conference - Day 2!2018-05-11T00:00:00Zhttps://simonhearne.com/2018/deltav-day2/<h2 id="tammy-everts-hunting-the-unicorn" tabindex="-1">Tammy Everts - Hunting the Unicorn <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#tammy-everts-hunting-the-unicorn" aria-hidden="true">#</a></h2>
<p>Tammy started researching user experience on the web in a laboratory setting - measuring individuals as they used websites. Now she works with SpeedCurve trying to connect the dots between real people and the stream of data being collected from Real User Monitoring.</p>
<blockquote>
<p>How do we scale empathy?</p>
</blockquote>
<p>As we collect millions or billions of user experience data points, how do we remind ourselves that there are real people on the other end of the data?</p>
<p>The average web user believes that they waste <strong>two days a year</strong> waiting for web pages to load.</p>
<p>We know that slow sites suck, but how do we define <strong>slow</strong> and how do we define <strong>suck</strong>? We are perception brokers. But perceptions are fluid and, well, perceptual. How can we measure something so dynamic?</p>
<p>Our traditional web performance metrics like start render, page load, speed index do not help us to measure perception!</p>
<p>The best UX metric needs to correlate to what users actually see, it needs to be easy to use and work out-of-the box in browsers, it should recognise that not all pixels and page elements are equal and should be customisable based on our knowledge of our sites.</p>
<p><strong>First meaningful paint (FMP)</strong> - times the paint after which the biggest above-the-fold layout change has happened or when web fonts are loaded, whichever comes later. Available in Chrome.</p>
<p><strong>Time to first interactive (TTFI)</strong> - when the page is first expected to be usable and will respond quickly to user interactions. Available in WebPageTest and mPulse.</p>
<p><strong>Hero rendering times</strong> - the time at which the largest image, largest background image and top-level heading render.</p>
<p>To help work out the best metrics, SpeedCurve created a <a href="http://lab.speedcurve.com/rendering/picker.php">test page</a> where you can see what metric correlates best with your perception of page load. The results unsurprisingly show that there is no metric that works for all sites, nor one that works for all people.</p>
<p><strong>Custom metrics</strong> are the only way to measure what you know is important to your page, for your users. E.g. time-to-first-scroll, time to product image. Unfortunately these aren't necessary accessible or easy to use.</p>
<p>Whatever you measure:</p>
<ol>
<li>Know your most meaningful metrics</li>
<li>Track them with synthetic and RUM</li>
<li>Don't focus on averages and medians</li>
<li>Set goals / budgets</li>
<li>Make yourself accountable with alerts and reports</li>
<li>Mind regressions</li>
</ol>
<h2 id="jeremy-keith-what-is-a-progressive-web-app" tabindex="-1">Jeremy Keith - What is a progressive web app? <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#jeremy-keith-what-is-a-progressive-web-app" aria-hidden="true">#</a></h2>
<p>There is a lot of confusion and misinformation about what progressive web apps (PWAs) are. They do not require a single-page application JavaScript framework!</p>
<p>Most of what makes a PWA is just what makes a got website - secure, fast and responsive. You can measure that a site has https, service worker and a web app manifest - that's what makes a <strong>measurable</strong> PWA.</p>
<blockquote>
<p>Add to home screen is a bit of a distraction for PWAs, don't worry too much about it</p>
</blockquote>
<p>Service Workers are the key part of a PWA, and they enable varying levels of offline functionality:</p>
<ol>
<li>An "offline" error page - e.g.</li>
<li>A full offline experience - e.g.</li>
<li>A user-initiated page download - e.g. <a href="https://una.im/">Una Kravet's</a> website</li>
</ol>
<blockquote>
<p>Should your site be progressive web app? Yes, it should</p>
</blockquote>
<p>Jeremy has <a href="https://abookapart.com/products/going-offline">published a book</a> about using Service Workers to create great offline experiences.</p>
<h2 id="mark-leach-and-andy-davies-fast-fashion" tabindex="-1">Mark Leach and Andy Davies - Fast Fashion <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#mark-leach-and-andy-davies-fast-fashion" aria-hidden="true">#</a></h2>
<p><a href="https://www.slideshare.net/AndyDavies/fast-fashion-how-missguided-revolutionised-their-approach-to-site-performance-deltav-conference-may-2018">Slides</a></p>
<p>Mark works at <a href="http://missguided.com/">Missguided.com</a> - a fast fashion brand aimed at trend-conscious women aged 18 - 29.</p>
<blockquote>
<p>Our job is to empower young women to feel strong and confident</p>
</blockquote>
<p>Mark's customers are all on social media, and are keen to complain when the website is slow.</p>
<p>When working with NCC Group Web Performance (now Eggplant) Mark was told that there could be an additional £1.2M through performance improvements.</p>
<p>When analysing the website, it was discovered that the median Android page load time was <strong>over 14 (fourteen) seconds</strong>.</p>
<p>Adding <code><link rel="preload"></code> for web fonts and fixing some issues with Optimizely brought the Android page load time down by two seconds.</p>
<p>Removing BazaarVoice improved load time by a further <strong>3 (three) seconds</strong> for Android devices. This improvement led to an increase in weekly revenue of <strong>56%</strong> 😮. When baselined against all other traffic with BazaarVoice still enabled, the net gain in revenue was <strong>26%</strong>.</p>
<p>When presenting to the board, initially they were pessimistic about the potential, but then saw the real data. Then they wanted more!</p>
<p>In October 2017, Missguided set a goal of a three second load time. Andy used <a href="https://github.com/danvk/source-map-explorer">source map explorer</a> to visualise the JavaScript and CSS bundles. The JavaScript bundle was reduced by 42%!</p>
<p>Missguided then hired someone to manage A/B testing and experiments to improve speed, as Optimizely could not be removed. This involved removing the bundled jQuery from the Optimizely bundle, removing old experiments and those for non-production environments.</p>
<p>Overall, median performance on Android was improved from 14 seconds to just over 5 seconds.</p>
<h2 id="lauren-younger-managing-performance-like-a-product" tabindex="-1">Lauren Younger - Managing Performance like a Product <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#lauren-younger-managing-performance-like-a-product" aria-hidden="true">#</a></h2>
<p>Performance and product management have a lot in common, they are both about improving conversion and revenue, retaining users, manage the user experience and understanding people.</p>
<p>Four basic principles for performance management:</p>
<ol>
<li>
<p>Understand
Market, competition, stakeholders, business KPIs.
<strong>Qualitative</strong> and quantitative data - using e.g. surveys and satisfaction scores. What does a 49% bounce rate mean? Were 49% of customers unsatisfied?</p>
</li>
<li>
<p>Define
Create a specific business case with SMART goals then get stakeholder buy-in.
Think beyond standard performance metrics (bounce rate, load time, session length), what about operations resource cost, global reach.</p>
</li>
<li>
<p>Prioritise
Prioritisation is hard, we're not very good at it and we tend to rely on opinion. Use data to create scores and ranking. It's easy to focus on the slowest pages, but you should use business information such as conversion impact to determine priority.</p>
</li>
<li>
<p>Communicate
We can't use the same communication methods when working with all of the stakeholders of your website. Marketing, operations and engineering all have different requirements and communication styles.</p>
</li>
</ol>
<p>Measure the results. What went well? What could have been improved in the process?</p>
<p><strong>Share your success</strong> and <strong>thank the team</strong>!</p>
<h2 id="dan-shappir-improving-performance-for-100-million-websites" tabindex="-1">Dan Shappir - Improving Performance for 100 Million Websites <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#dan-shappir-improving-performance-for-100-million-websites" aria-hidden="true">#</a></h2>
<p>Wix made performance a top priority feature in 2017. Performance has to be a feature in order to get resources, buy-in and a specification.</p>
<p>Wix runs >1,000 concurrent experiments and pushes to production around 200 times per day.</p>
<p>In order to protect performance, performance is measured during development, in pre-release and in production.</p>
<p>The biggest benefits seen by Wix (measured by change in first contentful paint):</p>
<p><strong>Server-Side Rendering</strong> - 70% improvement for mobile devices and 40% improvement for desktop. However the time between render and interactive became longer due to the additional delay in downloading the JavaScript bundle.</p>
<p><strong>Caching</strong> - before going crazy with service workers, ensure that your assets are actually cacheable at the client!</p>
<p><strong>Smart Bundling</strong> - dyanmically create app bundles depending on the features required on the site in question</p>
<p><strong>HTTP/2</strong> - 10 - 20% speed improvement by switching to HTTP/2.</p>
<p><strong>Resource Hints</strong> - Using preconnect, preload and prefetch gave Wix a 10% speed improvement.</p>
<p><strong>Progressive Rendering</strong> - by only rendering above-the-fold content in the first pass, Wix saw a 5 - 10% improvement.</p>
<p><strong>JavaScript Optimisation</strong> - Wix saw a 10% improvement by profiling and optimising their common JavaScript bundle.</p>
<p>Wix are in the process of transitioning from a JavaScript-driven layout to CSS Grid!</p>
<h2 id="robin-marx-quic-in-theory-and-practice" tabindex="-1">Robin Marx - QUIC in theory and practice <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#robin-marx-quic-in-theory-and-practice" aria-hidden="true">#</a></h2>
<p>Robin introduces the three main problems with HTTP/1.1:</p>
<ol>
<li>4xRTT (round-trip-time) connection setup</li>
<li>TCP head-of-line blocking</li>
<li>HTTP head-of-line blocking</li>
</ol>
<p>We can fix a few of these issues with TLS 1.3, fast-start and resumption. Leading to a single RTT connection establishment.</p>
<p>HTTP/2 removes the TCP head-of-line blocking, but requires a single connection which is susceptible to packet loss. Robin's research has shown that HTTP/2 is up to five times slower than HTTP/1.1 in high packet-loss environments.</p>
<p>We can't just upgrade TCP - it is too pervasive and there are too many pieces of technology which are too slow to upgrade (e.g. proxies). The solution to this is to use a different transport-layer protocol: UDP.</p>
<p>QUIC is built on top of UDP with custom packet-loss recovery logic. Solving TCP head-of-line blocking. Beyond that, it encompasses all of our network learning over the past three decades. By design, QUIC mitigates against all known DDoS attacks, prevents middlebox meddling through full transport-layer encryption. It also uses unique connection IDs to prevent connections closing down when IP addresses change, and allows communication over multiple networks (e.g. 4G and WiFi).</p>
<p>QUIC further improves on HTTP / TCP with custom congestion control.</p>
<p>Google have launched their own version of this protocol: <strong>gQUIC</strong>. A standardised version, <strong>iQUIC</strong> is expected to land at the end of 2018.</p>
<p>Google Search speed improvements: 8% on desktop, 3.6% on mobile. 16% improvement in 99th percentile.</p>
<p>20% less rebuffering on YouTube India, 14% faster on 4G, 20% slower than HTTP/1.1 with packet loss (HTTP/2 is 200% slower).</p>
<p>But, some research shows 75% lower throughput and QUIC costs 2x the CPU time on servers.</p>
<p>For QUIC connections on mobile devices, 58% of the time the network is faster than the CPU!</p>
<h2 id="harry-roberts-it-s-my-third-party-and-i-ll-cry-if-i-want-to" tabindex="-1">Harry Roberts - It's my (third) party and I'll cry if I want to! <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#harry-roberts-it-s-my-third-party-and-i-ll-cry-if-i-want-to" aria-hidden="true">#</a></h2>
<p><a href="https://speakerdeck.com/csswizardry/its-my-third-party-and-ill-cry-if-i-want-to">Slides</a>.</p>
<p>We use third-parties because they provide convenience, but this increases our exposure to risk.</p>
<p>Third-party content is inevitably added to provide value to the business, not the web users.</p>
<ol>
<li>Understand</li>
</ol>
<p><strong>Security</strong> - insecure content, man-in-the-middle attacks, compromised third-parties.</p>
<p><strong>Delays</strong> - network, third-party infrastructure, third-party runtime.</p>
<blockquote>
<p>Use WebPageTest's <strong>block</strong> and <strong>SPoF</strong> features to determine the impact of each third-party.</p>
</blockquote>
<p><strong>Failures</strong> - what happens when JavaScript fails?
Use the <strong>block request URL</strong> feature in Chrome DevTools.</p>
<p><strong>Runtime cost</strong> - defer expensive third-party JavaScript until after critical content.</p>
<p><strong>Who owns what?</strong> - use <a href="https://requestmap.webperf.tools/">requestmap.webperf.tools</a> to plot the third-party assets on your page. Harry has created a <a href="https://csswz.it/2rm29JY">Google Sheet</a> to help visualise the CSV output from request map!</p>
<p>In summary: use as few third-parties as possible, for as short a period of time as possible.</p>
<h2 id="dora-milataru-where-are-the-women" tabindex="-1">Dora Milataru - Where are the women? <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#dora-milataru-where-are-the-women" aria-hidden="true">#</a></h2>
<p>Dora leads the engineering team at <a href="http://ft.com/">FT.com</a> responsible for the changes required to be compliant with the General Data Protection Regulation (GDPR).</p>
<p>Globally, the Financial Times employs more women than men. Board representation is greater than 30% women, with a target of 50%.
This is a <strong>diversity quota</strong>, which can be dangerous. What happens when the quota is reached? Is that diversity done?</p>
<blockquote>
<p>Quotas may be the only way of achieving a world where quotas are obsolete</p>
</blockquote>
<p>Diversity is intersectional, discussing diversity along one dimension (gender, race, sexuality, religion, culture...)</p>
<p>If you search for why diversity is important in tech, you will find hundreds of articles which show that a diverse workforce improves business results. This helps get stakeholder buy-in, but does not help public opinion.</p>
<blockquote>
<p>Caring about diversity has to mean caring about people.</p>
</blockquote>
<p>Virtue signalling - showing off how <strong>good</strong> you are. Self-congratulation does not help society.</p>
<p>Social networks show that diversity alone doesn't help, especially in decision making processes. Inclusion is the glue that can fix this.</p>
<p><strong>We</strong> gravitate to people like <strong>us</strong>.</p>
<blockquote>
<p>Diversity is being invited to the party, inclusion is being glad that you're there</p>
</blockquote>
<p>The recent Stack Overflow developer survey shows that men don't rank diversity as important when selecting a job, while women and non-binary people rank diversity and working environment as more important than salary.</p>
<p>An internal <a href="https://www.forbes.com/sites/quora/2013/06/28/is-there-a-link-between-job-interview-performance-and-job-performance/#3df2990a458b">Google study</a> showed that there is zero correlation between a candidate's interview score and their future job performance.</p>
<p>Reach out to under-represented groups. Be honest. Celebrate success. You're not a saviour. Try to understand.</p>
<blockquote>
<p>Tolerating difference is not the same as celebrating diversity</p>
</blockquote>
<h2 id="leonie-watson-there-s-more-to-performance-than-meets-the-eye" tabindex="-1">Léonie Watson - There's more to performance than meets the eye <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#leonie-watson-there-s-more-to-performance-than-meets-the-eye" aria-hidden="true">#</a></h2>
<p>Screen readers</p>
<p>Accessibility APIs are provided by Operating Systems, these are exposed to assistive technologies (ATs) such as screen readers. As developers we don't have access to these APIs.</p>
<p>The Accessibility Tree provides a sister hierarchy to the browser's document object model (DOM). This contains element roles (e.g. <code>button</code>) and properties (e.g. <code>disabled</code>) as well as any text content (e.g. <code>Checkout Now</code>).</p>
<p>Microsoft Internet Explorer allows screen readers to inject themselves into the application process and communicates directly with the browser, while in MacOS the screen reader communicates with the browser across OS APIs.</p>
<p>The Microsoft Internet Explorer model provides a fast time to interaction, but is prone to instability. If the browser crashes it can take the screen reader down with it. Chrome and Firefox provide greater stability by proxying the accessibility API calls, Chrome also now caches the accessibility tree. This can lead to a slower initial TTI but fast subsequent page hits. This delay in TTI can be multiple seconds.</p>
<p>Firefox uses intelligent caching to improve TTI performance. Microsoft Edge, however, does not allow screen readers to inject into the process, depending on communication via the OS APIs. This makes communication between screen reader and Edge can be up to 50x slower than Internet Explorer.</p>
<p>Safari on MacOS is in-process, providing fast TTI.</p>
<h2 id="tobias-baldauf-25-faster-hotel-search-web-performance-trivago" tabindex="-1">Tobias Baldauf - 25% faster hotel search. Web performance? Trivago. <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#tobias-baldauf-25-faster-hotel-search-web-performance-trivago" aria-hidden="true">#</a></h2>
<ol>
<li>Research</li>
<li>Pitch</li>
<li>Build</li>
<li>Verify</li>
<li>Merge</li>
</ol>
<p>When launching a large marketing campaign in Brazil, Trivago discovered that Brazil performance was one second slower than the rest of the world. By analysing the performance by proxying into Brazil, they discovered that their CDN was not sending Brazil traffic to their Brazilian edge servers, but up to North America!</p>
<p>At Trivago, web performance is equal to user value, especially for emerging markets.</p>
<p>Feature teams are created from multiple disciplines around projects and given the freedom to build the feature.</p>
<p>A team of twelve people run a release controller tool: a real-user performance testing framework to test the impact of new features with canary traffic.</p>
<blockquote>
<p>The behaviour of your existing user base will not change instantly, they have trained behaviour and expectations. It took three months to see the positive impact of performance improvement.</p>
</blockquote>
<p>Part of the CDN rollout produced a delay on the hotel results display, which led to an increase in conversions! By keeping the UI clean while users interacted with the calendar widget and filters, users were more likely to continue their user journey.</p>
<p>Trivago has a performance team - they are not responsible for performance optimisation. The performance team owns the performance tooling and provides data to feature teams, enabling the developers to improve the performance of their own features.</p>
<p>Automated performance and accessibility testing is performed across at least five browser testing tools for every build, 30 - 50 times a day! Issues are added as build badges to the git branches.</p>
<blockquote>
<p>Web performance is a feature owned by everyone. Specialists consult and evangelise. Fast feedback loops help to iterate quickly and long-term data helps discover trends and define direction.</p>
</blockquote>
<blockquote>
<p>Beware of what to measure: business metrics may change unexpectedly when improving web performance. Choose the right tool for the job.</p>
</blockquote>
<blockquote>
<p>Web performance is equal to but not superior to the main features of a website sand can be temporarily traded in for learnings.</p>
</blockquote>
<h2 id="henri-helvetica-the-shape-of-the-web" tabindex="-1">Henri Helvetica - The shape of the web <a class="direct-link" href="https://simonhearne.com/2018/deltav-day2/#henri-helvetica-the-shape-of-the-web" aria-hidden="true">#</a></h2>
<p>The first browser, Mosaic, recently celebrated its 25th birthday.</p>
<blockquote>
<p>this software is going to change everything</p>
</blockquote>
<p>Mosaic achieved 65M new users in 18 months, now we have over 4 billion people online.</p>
<p>We do everything on the web, and it's reshaping everything we do.</p>
<blockquote>
<p>the web is the most hostile software engineering environment imaginable</p>
</blockquote>
<p>The next users on the web are coming from emerging markets with poor connectivity and low-end devices. Many users in these markets turn off their data connection to save money.</p>
<p>We need to treat <strong>offline</strong> not as an error case, but as a normal use case. This is the shape of the web. Service Workers and Progressive Web Apps are the greatest updates to browsers in a decade.</p>
Delta V Conference - Day 1!2018-05-10T00:00:00Zhttps://simonhearne.com/2018/deltav-day1/<h2 id="tim-kadlec-redefining-web-performance" tabindex="-1">Tim Kadlec - Redefining Web Performance <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#tim-kadlec-redefining-web-performance" aria-hidden="true">#</a></h2>
<p>There is a lot of under-appreciated creativity in technical work.
Definitions change - dictionaries are out of date as soon as they are printed - like JS frameworks.</p>
<p><strong>Network Constraint</strong> - 71% of global connections are 2G or 3G, we live with a rose-tinted view of the world.</p>
<p><strong>Device Constraint</strong> - new adoption is on low-end Android devices. 90th percentile JS payload is 1MB (compressed) / 7MB (uncompressed).</p>
<p>Performance cannot be fixed top-down (AMP / FIA / Apple News).
It took the New York Times 10 years to fix their performance problem.</p>
<p><strong>Speculative execution</strong> - a performance optimisation without considerations for the implications. (Spectre / Meltdown).</p>
<p><strong>Access & accessibility</strong> - he keystroke-level model for user performance time with interactive systems.
Performance is about the level of effort, human memory limits, task completion.</p>
<blockquote>
<p>Web Performance: how quickly users can complete their task.</p>
</blockquote>
<h2 id="sarah-dapul-weberman-the-first-10-months-of-a-dedicated-web-performance-team-at-pinterest" tabindex="-1">Sarah Dapul-Weberman - The first 10 months of a dedicated web performance team at Pinterest <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#sarah-dapul-weberman-the-first-10-months-of-a-dedicated-web-performance-team-at-pinterest" aria-hidden="true">#</a></h2>
<p><strong>Discovering the value of performance</strong>
Migration of user pages to React -> 20% performance gain. Led to 10 - 20% engagement increase.
Non-auth pages 30% faster -> 15% more signups, 10% increase in SEO traffic.</p>
<p><strong>No ownership of performance</strong> -> started dedicated performance teams for web, Android and iOS.</p>
<p><strong>Pinner Wait Time (PWT)</strong> as the key performance metric - a composite of the time to render the key image and the comments / metadata.</p>
<p>Get rid of vanity metrics - prove correlation with business goals or drop them.
Performance is a <strong>lever</strong> to change the business metrics.</p>
<p><strong>Impact Assessments</strong> - artificially delay key metrics and measure the impact on user behaviour / business metrics.</p>
<p><strong>PR campaign</strong> to publicise the performance team on all-hands calls.
Each critical page is tested multiple hundreds of times on every commit - the 90th percentile of PWT is taken and plotted, threshold-based alerts.</p>
<p><strong>"Perf Detective"</strong> performs a binary search with performance testing (like git bisect) to find the exact commit which caused the regression.</p>
<p><strong>Changes are prototyped first</strong> to provide estimated performance wins - before development starts! Once the projects are decided on they are tested in an A/B framework which has performance and engagement metrics built in!
A/B test results are segmented by geography, user behaviour profiles etc.</p>
<p>The team beat the original goal of 14% improvement by an additional 20%!
Vision: five pillars:</p>
<ol>
<li>Scalability (team) - we can’t be the only people working on performance. Enable product teams to manage performance.</li>
<li>Ownership - each team owns their surface performance. Decentralised monitoring.</li>
<li>Knowledge base - create a central place to document what performance looks like at Pinterest</li>
<li>Tooling - create / purchase the tools which developers want to use</li>
<li>Strategy - performance team will own the performance strategy - where to focus, tool management. Distribute the work.</li>
</ol>
<h2 id="rhys-evans-speeding-up-ft-com-without-slowing-down" tabindex="-1">Rhys Evans - Speeding up <a href="http://ft.com/">FT.com</a>, without slowing down <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#rhys-evans-speeding-up-ft-com-without-slowing-down" aria-hidden="true">#</a></h2>
<p><a href="https://speakerdeck.com/player/845b3dbdbfab441b86a059966bb68d69">Slides</a> | <a href="https://twitter.com/wheresrhys">@wheresrhys</a></p>
<p>The old <a href="http://ft.com/">FT.com</a> site (Falcon) was slow and high-risk. One release a month!</p>
<blockquote>
<p>If we ever need to start from scratch again, we've gone badly wrong</p>
</blockquote>
<p>For the new site, the team insisted on control of all web content, including third-parties.</p>
<p>Deploy local -> production in 10 minutes using CircleCI and Heroku. Feature flags enable roll-out of features to live traffic.</p>
<p>~100 microservices create the <a href="http://ft.com/">FT.com</a> back-end stack.
Componentisation at the front-end using <a href="http://origami.ft.com/">Origami</a>.</p>
<blockquote>
<p>These practices free us to release quality software quickly, with confidence</p>
</blockquote>
<p>Microservices add extra latency to user requests due to dependency chains. <a href="http://ft.com/">Ft.com</a> uses multiple caching layers to resolve that.</p>
<p>Preflight enables intelligent caching at the edge. But this itself is another performance bottleneck with multiple microservices! <strong>Every service</strong> is measured, medians and percentiles are used and the number of timeouts are counted and plotted too.</p>
<p><strong>Code impatiently</strong> - HTTP agent keep alive to create connection pools. Pessimistic timeouts - fail fast and provide error details.</p>
<p>Fast releases reduced asset reuse - with no inlined CSS or shared JS bundles!</p>
<p>The singular focus on performance made releases harder, alienated the development team.</p>
<blockquote>
<p>Performance is not a core front-end skill. It's weird, complex and <strong>optional</strong></p>
</blockquote>
<p>Performance optimisations should be discussed with the whole team, especially junior developers. Optimisations should be released early and often, tested and balanced with developer convenience.</p>
<blockquote>
<p>You don't have to be perfect to get results!</p>
</blockquote>
<p><a href="https://medium.com/ft-product-technology/making-a-request-to-the-financial-times-b2119a2f422d">Further reading!</a></p>
<h2 id="seren-davies-and-bruce-lawson-this-is-for-everyone" tabindex="-1">Seren Davies & Bruce Lawson - This is for everyone <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#seren-davies-and-bruce-lawson-this-is-for-everyone" aria-hidden="true">#</a></h2>
<h3 id="seren" tabindex="-1">Seren <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#seren" aria-hidden="true">#</a></h3>
<p>Accessibility issues are much broader than we consider, with two broad topics: <strong>permanent</strong> and <strong>temporary / situational</strong>.</p>
<p><strong>Cognitive Accessibility</strong> e.g. dyslexia, ADHD and epilepsy
Animations should add value and not distract.</p>
<p><strong>Drunk</strong>
Use high contrast colours and large text. City mapper increases the size of the "get me home" button as the evening progresses!</p>
<p><strong>Touch or movement</strong> - e.g. broken arm, holding a baby.</p>
<p>Link areas should be larger than the text - provide large tap targets.</p>
<p><strong>Sight</strong> - beyond screen-readers, including colour-blindness.
Text on images is really tricky - ensure that contrast ratio is preserved for all characters.</p>
<p><strong>Deaf / hard of hearing</strong> - always add connections</p>
<h3 id="bruce" tabindex="-1">Bruce <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#bruce" aria-hidden="true">#</a></h3>
<p>4 Billion people are not on the web - including 2 billion from India and China, and 51 million in the USA!</p>
<p>We need to sell into growing markets. These markets are overwhelming getting online with low-powered Android devices, with limited RAM on 2G networks. The infrastructure supporting mobile networks takes decades, while new devices take months.</p>
<p>A standard-sized Android app will take 30 minutes to download on a 2G connection, except it's unlikely that the download will succeed during that time.</p>
<p>Progressive Web Apps are the solution. Flipkart lite increased returning visitors by 40% when they released their PWA.</p>
<p>Images still account for >50% of page weight so that's a good place to start!</p>
<p>This matters not to the people loading your site from Silicon Valley, it's for the people who pay per MB.</p>
<p><a href="http://worldreader.org/">Worldreader.org</a></p>
<blockquote>
<p>If you want to liberate a country, give it the internet</p>
</blockquote>
<p>The web is for everybody.</p>
<h2 id="inian-parameshwaran-chrome-real-user-experience-report" tabindex="-1">Inian Parameshwaran - Chrome Real User Experience Report <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#inian-parameshwaran-chrome-real-user-experience-report" aria-hidden="true">#</a></h2>
<p>Measuring speed - Synthetic & RUM
HTTP Archive is the archive for synthetic tests, CrUX is the archive for RUM data.</p>
<p>Rather than absolute values, CrUX stores histograms. Inian defines <strong>Site Experience Benchmark (SEB)</strong> as the fraction of users whose time to first contentful paint is under one second.</p>
<p>SEB reduces by 36% on average between desktop and mobile.</p>
<p>On average, SEB is 225% better on non-framework sites compared with those using a single-page app frameworks. This grows to 486% when isolating to mobile traffic.</p>
<p>Using RUM data gives a much wider perspective on geography and device diversity than synthetic testing, so you can use it to help inform technology choices. E.g. React server-side rendering improves SEB by 47%, 69% on mobile.</p>
<p><a href="http://google.com/">google.com</a> is 40% faster than <a href="http://google.com.cn/">google.com.cn</a> for visitors in China. 🤨</p>
<p>Lack of information on the number of data points in CrUX makes it hard to do cross-site analysis.</p>
<h2 id="laura-sheridan-and-nick-webb-web-performance-at-n-brown-it-s-a-business-affair" tabindex="-1">Laura Sheridan & Nick Webb - Web performance at N Brown, it's a business affair <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#laura-sheridan-and-nick-webb-web-performance-at-n-brown-it-s-a-business-affair" aria-hidden="true">#</a></h2>
<p>N Brown are a fashion brand group that focuses on under-served groups - those that are larger and / or older.</p>
<p>First online store in 1999 as a small IT project. Now 75% online sales, 76% of traffic is smartphone. 4 million sessions per week!</p>
<p>After getting inspired at Velocity Berlin 2011, N Brown launched a project to remove duplicate scripts, set a weight budget for the homepage and purchased a StrangeLoop on-premise FEO solution.</p>
<p>Making the business understand performance - using RUM data to correlate page load time with bounce rate. Then created department-specific metrics, e.g. total image weight for the design team.</p>
<p>Laura held roadshows to share performance data for each of the 14 brand websites, with tips on how to improve. They also developed a web performance action plan - including scheduled work on specific operating systems, device types and page types.</p>
<p>To ensure performance does not regress, a weekly report is produced for each brand, device and page group combination. This is used to encourage internal competition and provide insight into regressions.</p>
<p>RUM data allows N Brown to modify their development plans between brands, based on the user profiles of the brand customers and their web environment.</p>
<p>Tag mapping for identification of what's on the site, as well as analysis for GDPR compliance. This led to a tag governance process.</p>
<blockquote>
<p>The right visualisation can do our job for us</p>
</blockquote>
<p>48/52 split between iOS and Android traffic, but sales are much lower on Android. To help fix the issue, rules were added to deliver the <strong>mobile</strong> version of the site to Android tablets with display sizes under ten inches.</p>
<p>As N Brown re-platform, there are weekly team lead meetings to discuss performance and a UX Director to ensure the performance of the new sites.</p>
<h2 id="uve-making-a-progressive-web-application" tabindex="-1">Uve - Making a Progressive Web Application <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#uve-making-a-progressive-web-application" aria-hidden="true">#</a></h2>
<p>This was a live coding demo - <a href="https://medium.com/samsung-internet-dev/a-beginners-guide-to-making-progressive-web-apps-beb56224948e">here's a relevant blog post!</a></p>
<h2 id="ian-feather-frontend-resilience" tabindex="-1">Ian Feather - Frontend Resilience <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#ian-feather-frontend-resilience" aria-hidden="true">#</a></h2>
<p><strong>Resilience is</strong> function in a hostile environment.</p>
<p>We don't ship compiled products - there are many components we have to ship over the network to create a functional product.</p>
<p>We tend to develop in the ideal environment for a great user experience, but we can't guarantee that. We need to guarantee the most basic level of acceptable UX.</p>
<h3 id="how-systems-can-fail" tabindex="-1">How systems can fail <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#how-systems-can-fail" aria-hidden="true">#</a></h3>
<p><strong>HTTPS</strong> is a must-have to protect you from malicious interference. Third-party scripts are a nightmare.</p>
<blockquote>
<p>Pick your third-parties and trust them well.</p>
</blockquote>
<p>13 million JavaScript requests fail per month (1%) - that's a greater proportion of page views than IE11. 9% of visitors use content blockers, and 4% of users don't load web fonts - 40 million page views per month with no fonts.</p>
<blockquote>
<p>Know what's broken before Twitter does</p>
</blockquote>
<h3 id="how-we-can-design-for-failure" tabindex="-1">How we can design for failure <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#how-we-can-design-for-failure" aria-hidden="true">#</a></h3>
<p>Have a working user experience in the first request. I.e. know what the critical part of your user experience is and ensure that it can be usable with the first HTML response.</p>
<p>Tell your developers when things break, but don't tell your users unless you have to. If you have to tell your users - give them an informative message and reassurance.</p>
<h3 id="how-we-can-mitigate-failures" tabindex="-1">How we can mitigate failures <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#how-we-can-mitigate-failures" aria-hidden="true">#</a></h3>
<p><strong>Lock your dependencies</strong> - use sub-resource integrity (SRI) for all third-party assets. But this then requires a well defined update process between you and your third-parties.</p>
<p><strong>Build in redundancy</strong> - have two of everything, and use a proxy where possible to create an active / active asset delivery system.</p>
<p>Buzzfeed has <strong>Plan Z</strong> - a static copy of <a href="http://buzzfeed.com/">buzzfeed.com</a> hosted both on AWS S3 and Google Cloud Platform and updated every few minutes. If everything is on fire, the DNS is updated to point at a static backup!</p>
<p>Serve stale content - use cache control headers to enable the CDN to continue to deliver assets when origin is unavailable.</p>
<h3 id="how-we-can-learn-from-our-failures" tabindex="-1">How we can learn from our failures <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#how-we-can-learn-from-our-failures" aria-hidden="true">#</a></h3>
<p>Run blameless postmortems religiously to ensure that knowledge is gained from every failure. Consider how incidents were handled as a team and how it can be prevented from happening in the future.</p>
<p>Run fire drills and chaos testing to give the team practice on responding to incidents.</p>
<h2 id="ada-rose-cannon-new-web-technology" tabindex="-1">Ada Rose Cannon - New web technology <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#ada-rose-cannon-new-web-technology" aria-hidden="true">#</a></h2>
<p>This one is best viewed the way Ada presented it, don't forget to <a href="https://ada.is/grid-slides/new-web/">view-source</a>.</p>
<h2 id="denys-mishunov-debugger-for-developers" tabindex="-1">Denys Mishunov - debugger; for developers <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#denys-mishunov-debugger-for-developers" aria-hidden="true">#</a></h2>
<h3 id="perfectionism" tabindex="-1">Perfectionism <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#perfectionism" aria-hidden="true">#</a></h3>
<p>There are two types of perfectionism: healthy and unhealthy.</p>
<p>Steve Jobs was not satisfied with any of the 2,000 shades of beige given as options for the first Apple computer. This is perfectionism paralysis.</p>
<p>Perfectionists tend to pick small details to work on, where there is a guarantee that they can achieve the perfect result.</p>
<p>Unnecessary tasks (the cherry on top). Near the end of the project you might think of a small feature that would bring the project a little closer to perfect.</p>
<p>We cannot force perfectionists to stop being perfectionists, but we can help to turn negative productivity into positive.</p>
<p>### Imposter Syndrome</p>
<p>2 out of 5 successful people suffer from imposter syndrome, and 70% of the general population have experienced it at some point in the career.</p>
<p>You think that your success is due to luck, others might discover that you are not as intelligent as they think you are or that everyone is better than you.</p>
<p>True frauds rarely experience imposter syndrome.</p>
<blockquote>
<p>The trouble with the world is that the stupid area cocksure and the intelligent are full of doubt</p>
</blockquote>
<p>Feeling like an imposter is a symptom of gaining expertise.</p>
<p>Measure yourself against you, not others. Communicate your fears and feelings.</p>
<h3 id="long-hours" tabindex="-1">Long Hours <a class="direct-link" href="https://simonhearne.com/2018/deltav-day1/#long-hours" aria-hidden="true">#</a></h3>
<p>One of the most common death-bed regrets is <strong>working too much</strong>.</p>
<p>There are two types of long hours: temporary and permanent. Temporary long hours are associated with hard-working people, while permanent long hours indicates workaholics.</p>
<p>Stress and burnout are eventually harmful to productivity, even if there is a temporary increase at the beginning.</p>
<p>Every year <a href="https://en.m.wiktionary.org/wiki/guolaosi">Guolaosi</a> costs China 1,600 people per day due to overwork.</p>
Third-Party Content - the weak link?2018-01-14T00:00:00Zhttps://simonhearne.com/2018/third-party-weak-links/<p><lite-youtube videoid="9ol9MNGqVOM" playlabel="Play: Third-Party Content - the weak link?"></lite-youtube></p>
<p><a class="link-button no-link" href="https://docs.google.com/presentation/d/1pUo2SrWPvJ5vwYIbpebqFqnhAwGZ2pVryKP2kzqA_Uw/edit?usp=sharing">View Slides</a></p>
Twitter Lite isn't as good as the app, and that's okay2017-04-07T00:00:00Zhttps://simonhearne.com/2017/twitter-pwa/<h2 id="the-promise-of-pwas" tabindex="-1">The promise of PWAs <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#the-promise-of-pwas" aria-hidden="true">#</a></h2>
<p>Progressive Web Apps bring a native-like application experience to websites. This is achieved through a combination of specifications like the web application manifest, service worker, web push API and (soon) webAPKs.</p>
<p>The advantages of using a website over a native application are numerous for consumers: faster install, more rapid updates and a significant reduction in the storage required. There are also advantages for application publishers: less friction to install (it's just a URL!), instant updates (no app store update process) and they are easier to develop for cross-platform audiences.</p>
<p>Normal websites can be upgraded to add great features such as a good offline experience and a more immersive browser view. This means that PWAs aren't just an alternative to native applications, it makes sense to roll these features out on websites anyway.</p>
<br />
<p>Have you ever started to install an application, then stop because it requested permission to record audio, spam all of your contacts, install a trojan horse or turn your phone pink? The web has a well developed permissions model for things like access to the camera, vibration, location and sensors. A PWA inherits all of this development in web permissions.</p>
<blockquote>
<p>I want to point out here that, although I am picking at the flaws in Twitter Lite, I have great respect for the team that created it. Twitter releasing a progressive web app is such a positive sign for the web. I'm also aware that I am not the key audience of Twitter Lite: I have a phone that cost an eye-watering amount to buy and consistent access to 4G coverage. The issues I highlight below are minor compared to the increased reach provided by Twitter Lite: to users who cannot access the latest devices, who may only have 2G network coverage and might rely solely on their phone for communication. Twitter Lite is a positive step for the web! Browsers just need to catch up with what we are now expecting of them.</p>
</blockquote>
<h2 id="twitter-lite" tabindex="-1">Twitter Lite <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#twitter-lite" aria-hidden="true">#</a></h2>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/gZg_z2iy64-600.avif 600w, https://simonhearne.com/img/gZg_z2iy64-900.avif 900w, https://simonhearne.com/img/gZg_z2iy64-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/gZg_z2iy64-600.webp 600w, https://simonhearne.com/img/gZg_z2iy64-900.webp 900w, https://simonhearne.com/img/gZg_z2iy64-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/gZg_z2iy64-600.jpeg 600w, https://simonhearne.com/img/gZg_z2iy64-900.jpeg 900w, https://simonhearne.com/img/gZg_z2iy64-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/gZg_z2iy64-600.jpeg" width="1200" height="656" /></picture><figcaption>104MB of storage for the Twitter application</figcaption></figure>
<p>Twitter's native application has over 500M downloads on the Google Play Store, and presumably a similar number on the App Store. Twitter has been experimenting with their mobile web experience over the past six months, adding PWA features to groups of users and measuring the response. On April 6 Twitter announced 'Twitter Lite', a fully-fledged progressive web app. The <a href="https://blog.twitter.com/2017/introducing-twitter-lite">marketing</a> angle around the release is that it will improve the experience for users with low bandwidth mobile connections, especially those in Asia Pacific, Latin America and Africa. I don't fit that demographic, I am extremely fortunate to have a smartphone that costs an eye watering amount to buy and I have 4G internet most of the time. But I am a massive PWA fan, and couldn't continue to call myself that without switching to the Twitter PWA!
I must point out that the PWA <em>is</em> a massive step for Twitter users worldwide and for the industry. But there is also a lot of room for improvement both with the PWA and browser implementations.</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Introducing Twitter Lite on mobile web! 📱<br /><br />Loads quickly, takes up less space, and is data-friendly. Learn more: <a href="https://t.co/Zd825WOdQz">https://t.co/Zd825WOdQz</a> <a href="https://t.co/l1n0cYJuPc">pic.twitter.com/l1n0cYJuPc</a></p>— Twitter (@Twitter) <a href="https://twitter.com/Twitter/status/849866660882206721">April 6, 2017</a></blockquote>
<p>I jumped on the bandwagon, why would I want a massive native application, with frequent ~70MB updates, when the mobile website will give me the same experience?!</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Goodbye native app, hello fast web experience! <a href="https://t.co/xfQ3tY0hYW">pic.twitter.com/xfQ3tY0hYW</a></p>— Simon Hearne (@simonhearne) <a href="https://twitter.com/Twitter/status/849970819258343424">April 6, 2017</a></blockquote>
<blockquote>
<p>I'm sad to say that I have now reverted to the native application, after less than 24 hours of using the PWA.</p>
</blockquote>
<h2 id="browser-dependence" tabindex="-1">Browser dependence <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#browser-dependence" aria-hidden="true">#</a></h2>
<p>Before I discuss why I've gone back to the native application, it's worth discussing the inherent limitations of PWAs. The core PWA specifications of <a href="http://caniuse.com/#feat=serviceworkers">service worker</a>, <a href="http://caniuse.com/#feat=web-app-manifest">web app manifest</a> and <a href="http://caniuse.com/#feat=push-api">web push</a> are currently only supported on mobile devices running Android.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/n5FdPF-WWP-600.avif 600w, https://simonhearne.com/img/n5FdPF-WWP-900.avif 900w, https://simonhearne.com/img/n5FdPF-WWP-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/n5FdPF-WWP-600.webp 600w, https://simonhearne.com/img/n5FdPF-WWP-900.webp 900w, https://simonhearne.com/img/n5FdPF-WWP-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/n5FdPF-WWP-600.jpeg 600w, https://simonhearne.com/img/n5FdPF-WWP-900.jpeg 900w, https://simonhearne.com/img/n5FdPF-WWP-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/n5FdPF-WWP-600.jpeg" width="1200" height="312" /></picture><figcaption>PWA Support</figcaption></figure>
<p>This also means that the performance of the PWA is dependent on browser performance. Native applications can be optimised at a code-level to give the best performance, while PWAs have to rely on the browser to interpret the HTML, CSS and JavaScript provided. This may mean that PWAs perform more poorly than native applications.</p>
<h2 id="things-that-just-don-t-work" tabindex="-1">Things that just don't work <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#things-that-just-don-t-work" aria-hidden="true">#</a></h2>
<p>There are five key issues I have with the twitter PWA which have led me back to the native application, for now.</p>
<h3 id="1-text-boxes-are-tiny-and-broken" tabindex="-1">1) Text boxes are tiny, and broken <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#1-text-boxes-are-tiny-and-broken" aria-hidden="true">#</a></h3>
<p>I use an increased font-size in my browser configuration, this is because so many websites use tiny font sizes and disable zooming on mobile. It's easier for me to just have bigger text everywhere. Unfortunately this breaks the way Twitter's input boxes grow with text, making it really awkward to edit or review a tweet.</p>
<figure align="center">
<video autoplay="" muted="" loop="" controls="true" playsinline="" style="width:80%; max-width:480px;" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_480,q_auto/v1612776931/videos/text.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_480,q_auto/v1612776931/videos/text.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_480,q_auto/v1612776931/videos/text.mov" type="video/mov" />
<source src="https://simonhearne.com/images/text.webm" type="video/webm" />
<source src="https://simonhearne.com/images/text.mp4" type="video/mp4" />
</video>
<figcaption>Text boxes are too small and do not overflow correctly</figcaption>
</figure>
<h3 id="2-push-notifications-are-less-reliable" tabindex="-1">2) Push notifications are less reliable <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#2-push-notifications-are-less-reliable" aria-hidden="true">#</a></h3>
<p>I noticed a few times during my trial of the PWA that I would open Twitter to find unread notifications, but I had received no push notifications. On re-installing the native application I often get more notifications from the application than the PWA. The Web Push API depends on a <a href="https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/sending-messages">third-party server</a> to send notifications, and it's generally fire-and-forget with no ability to re-send failed notifications.
Also frustrating is the lack of grouping of notifications, a popular tweet can quickly fill up your notifications!</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/RBZ7Yh-jyX-600.avif 600w, https://simonhearne.com/img/RBZ7Yh-jyX-900.avif 900w, https://simonhearne.com/img/RBZ7Yh-jyX-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/RBZ7Yh-jyX-600.webp 600w, https://simonhearne.com/img/RBZ7Yh-jyX-900.webp 900w, https://simonhearne.com/img/RBZ7Yh-jyX-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/RBZ7Yh-jyX-600.jpeg 600w, https://simonhearne.com/img/RBZ7Yh-jyX-900.jpeg 900w, https://simonhearne.com/img/RBZ7Yh-jyX-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/RBZ7Yh-jyX-600.jpeg" width="1200" height="1040" /></picture><figcaption>Fewer notifications from the PWA</figcaption></figure>
<p>A small secondary issue is that newline characters are not escaped in the notifications. I'm not sure if this behaviour changes between devices, if so this would be really tough to test. It makes the PWA feel a bit immature.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/hPkpdv3Lid-600.avif 600w, https://simonhearne.com/img/hPkpdv3Lid-900.avif 900w, https://simonhearne.com/img/hPkpdv3Lid-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/hPkpdv3Lid-600.webp 600w, https://simonhearne.com/img/hPkpdv3Lid-900.webp 900w, https://simonhearne.com/img/hPkpdv3Lid-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/hPkpdv3Lid-600.jpeg 600w, https://simonhearne.com/img/hPkpdv3Lid-900.jpeg 900w, https://simonhearne.com/img/hPkpdv3Lid-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/hPkpdv3Lid-600.jpeg" width="1200" height="402" /></picture><figcaption>New lines are not escaped in notifications.</figcaption></figure>
<h3 id="3-portrait-images-aren-t-re-oriented" tabindex="-1">3) Portrait images aren't re-oriented <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#3-portrait-images-aren-t-re-oriented" aria-hidden="true">#</a></h3>
<p>When attempting to add a portrait picture, which the native application uploads with the correct orientation, the PWA has it sideways. This wouldn't be such an issue if the image could then be rotated in the PWA, but it cannot. As such this is a killer bug for me.</p>
<p>Another slight irritation is shown in the video below. There is no indication to the user that the image is being uploaded! This is on wifi and there is an eight second delay with no user feedback, while the whole browser is locked up. Considering that the PWA is designed for users with low bandwidth mobile connections, this delay could be significantly longer.</p>
<figure align="center">
<video autoplay="" muted="" loop="" controls="true" playsinline="" style="width:80%; max-width:480px;" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/image.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/image.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/image.mov" type="video/mov" />
</video>
<figcaption>Images aren't correctly rotated, and there's no way to fix it</figcaption>
</figure>
<h3 id="4-pull-to" tabindex="-1">4) Pull-to-... <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#4-pull-to" aria-hidden="true">#</a></h3>
<p>The native Twitter application has trained me to pull-to-update on the feed. Pulling down in other screens does nothing. Unfortunately, due to the dependence on the browser, this behaviour is not consistent in the PWA. It will reload the whole page if you pull on most pages. Implementing pull-to-update across the whole PWA might make this more consistent.</p>
<figure align="center">
<video autoplay="" muted="" loop="" controls="true" playsinline="" style="width:80%; max-width:480px;" preload="metadata">
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/ptr.webm" type="video/webm" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/ptr.mp4" type="video/mp4" />
<source src="https://res.cloudinary.com/simonhearne/video/upload/w_500,q_auto/v1612776931/videos/ptr.mov" type="video/mov" />
</video>
<figcaption>Pull-to-update behaviour is inconstistent.</figcaption>
</figure>
<h3 id="5-sharing" tabindex="-1">5) Sharing <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#5-sharing" aria-hidden="true">#</a></h3>
<p>One of the key features of the web is being able to share content. The native Twitter application adds sharing intents to menus, so that you can share content via tweets or direct messages from any other application (or browser) on your device. This is missing in the PWA which makes day-to-day use significantly more tricky - copy to clipboard, back to homescreen, open PWA, click the tweet button, tap-and-hold, paste text.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/-jZScI1zJZ-600.avif 600w, https://simonhearne.com/img/-jZScI1zJZ-900.avif 900w, https://simonhearne.com/img/-jZScI1zJZ-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/-jZScI1zJZ-600.webp 600w, https://simonhearne.com/img/-jZScI1zJZ-900.webp 900w, https://simonhearne.com/img/-jZScI1zJZ-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/-jZScI1zJZ-600.jpeg 600w, https://simonhearne.com/img/-jZScI1zJZ-900.jpeg 900w, https://simonhearne.com/img/-jZScI1zJZ-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/-jZScI1zJZ-600.jpeg" width="1200" height="1240" /></picture><figcaption>Lack of integrated share options is frustrating.</figcaption></figure>
<h2 id="minor-annoyances" tabindex="-1">Minor annoyances <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#minor-annoyances" aria-hidden="true">#</a></h2>
<ol>
<li>Can't have multiple accounts logged in simultaneously</li>
<li>Saved searches are gone</li>
<li>No option to add to list</li>
<li>No swipe actions</li>
<li>No tweet analytics</li>
</ol>
<p>As the PWA is <a href="https://blog.twitter.com/2017/how-we-built-twitter-lite">built on the Twitter API</a> its functionality is limited to what is currently available to all other Twitter clients.</p>
<h2 id="conclusion" tabindex="-1">Conclusion <a class="direct-link" href="https://simonhearne.com/2017/twitter-pwa/#conclusion" aria-hidden="true">#</a></h2>
<p>So I've moved back to the native application. Of course that doesn't mean I can't use the PWA, as I can just open up <a href="https://mobile.twitter.com/">mobile.twitter.com</a> and get the full PWA experience. The Twitter PWA is trying to do a lot, while browsers are still getting to grips with implementing the specifications. Having Twitter invested in the PWA ecosystem should really help to push the development along.</p>
<p>Committing to PWAs will continue to be a tough sell until Safari on iOS supports service worker, web push and web app manifests.</p>
Your Analytics Lies to You2017-03-29T00:00:00Zhttps://simonhearne.com/2017/analytics-lie/<h2 id="the-site-speed-fallacy" tabindex="-1">The site speed fallacy <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#the-site-speed-fallacy" aria-hidden="true">#</a></h2>
<p>In a recent webinar, <a href="https://twitter.com/AndyDavies">Andy Davies</a> and I asked a poll of the attendees: where do you get your site speed data? Bear in mind that the majority of the attendees on the webinar were from Ecommerce or Digital Management. The results were just about what we were expecting, over half of attendees use web analytics to get their speed data:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/3FzumA_fPl-600.avif 600w, https://simonhearne.com/img/3FzumA_fPl-900.avif 900w, https://simonhearne.com/img/3FzumA_fPl-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/3FzumA_fPl-600.webp 600w, https://simonhearne.com/img/3FzumA_fPl-900.webp 900w, https://simonhearne.com/img/3FzumA_fPl-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/3FzumA_fPl-600.jpeg 600w, https://simonhearne.com/img/3FzumA_fPl-900.jpeg 900w, https://simonhearne.com/img/3FzumA_fPl-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/3FzumA_fPl-600.jpeg" width="1200" height="354" /></picture><figcaption>Poll showing that 58% use web analytics to get site speed data.</figcaption></figure>
<blockquote>
<p>Miss the webinar? You can <a href="https://attendee.gotowebinar.com/recording/224707572878182658">watch the recording here</a></p>
</blockquote>
<p>A further 29% of attendees use synthetic monitoring as their source of site speed data. While synthetic monitoring is great for measuring operational performance and tracking changes over time, it gives little indication of the real user experience. Only 2% of attendees stated that they use real user monitoring.</p>
<p>Using analytics for site speed is logical, if you have a website you will have analytics. Access to products like Google Analytics is widespread throughout an organisation, and it provides reporting on site speed data. Google Analytics (GA) is actually the best we've seen for reporting site speed. You can view multiple timing metrics and even segment by device type or location. Adobe Analytics has no default site speed data, you have to manually add it to the analytics code, once it's being collected there is very little you can do with it.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Iu3mmfhP94-600.avif 600w, https://simonhearne.com/img/Iu3mmfhP94-900.avif 900w, https://simonhearne.com/img/Iu3mmfhP94-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Iu3mmfhP94-600.webp 600w, https://simonhearne.com/img/Iu3mmfhP94-900.webp 900w, https://simonhearne.com/img/Iu3mmfhP94-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/Iu3mmfhP94-600.jpeg 600w, https://simonhearne.com/img/Iu3mmfhP94-900.jpeg 900w, https://simonhearne.com/img/Iu3mmfhP94-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Iu3mmfhP94-600.jpeg" width="1200" height="550" /></picture><figcaption>GA quietly samples site speed data to 1%, independent of traffic sampling.</figcaption></figure>
<p>GA's site speed data is flawed. The data is sampled, limited by default to <a href="https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate">no more than 1% of pageviews</a> even for Analytics 360 paid accounts. This sample rate can be changed to 100%, although that will dramatically skew your data towards traffic in the early hours of the morning, <a href="https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings#sampling_considerations">especially if your site attracts over 1M pageviews per day</a>. Even if GA collected 100% of site speed data, it cannot be correlated with any other metric. The speed data is aggregated and averaged by time and not linked with sessions. As such it is impossible to determine the user experience of a whole session, or to correlate speed with goal completion.</p>
<figure align="center">
<code>ga('create', 'UA-XXXX-Y', {'siteSpeedSampleRate': 100});</code>
<figcaption>Set a 100% sample rate for GA site speed. p.s. <a href="https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings#sampling_considerations">do not do this</a>!</figcaption>
</figure>
<p>Most of my job as a performance consultant is convincing companies that site speed is critical to their business, GA does not make that easy!</p>
<h2 id="phantom-bounces" tabindex="-1">Phantom bounces <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#phantom-bounces" aria-hidden="true">#</a></h2>
<p>Anyway, so you've changed your sampling rate and got 100% of your pageviews sending site speed data. The average page load time is seven seconds, that seems pretty slow to you. Bounce rate is just over 50%, that seems pretty bad too.
It's scary to think about it, but your bounce rate is likely much higher than 50%. Bounces are sessions which have only one pageview, it doesn't count sessions which have zero pageviews... Bear with me.</p>
<p>A recent <a href="https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/">study by Google</a> compared DoubleClick and Google Analytics data to find that 53% of mobile ad clicks never result in a pageview, when the page has a load time of three seconds or more. Your analytics shows you that your mobile page load time is eight seconds, how many users who click on a link to your site <em>never</em> result in a pageview?</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Ip6tUAUaL_-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Ip6tUAUaL_-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Ip6tUAUaL_-600.jpeg" width="600" height="65" /></picture><figcaption>GA beacons cancelled when user navigates away during a slow page load.</figcaption></figure>
<p>These non-pageviews are <em>phantom bounces</em>. They don't appear anywhere in your analytics, but they could be a significant proportion of your traffic. What's worse, your marketing budget is being spent on traffic which may never reach your landing pages.</p>
<h2 id="device-representation" tabindex="-1">Device representation <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#device-representation" aria-hidden="true">#</a></h2>
<p>Most of my clients base their device testing strategy on their analytics. I vividly remember a story I was told by an employee at a large UK retailer: when first joining the mobile team as a contract resource she questioned why there was no testing on the first generation iPad. She was told that their analytics showed no traffic from such an old device (three years after it was launched) so there was no need to test on it. The sceptical contractor got out her very-much-still-working iPad 1 and loaded the homepage. Safari crashed. No analytics event was recorded.</p>
<p>This is a textbook case of the <a href="https://en.wiktionary.org/wiki/tail_wagging_the_dog">tail wagging the dog</a>.</p>
<p>It is common for my clients to focus their attention on flagship devices from Apple and Samsung, "because all of our customers have them". Unfortunately your analytics is coming up short again. For one, GA will group all iPhone models into a homogenous 'iPhone' device; it does the same with iPads. How can you develop a solid device lab when you don't know which of the eleven iPhone models generate the most traffic?</p>
<p>Just because Apple and Samsung make up for 80% of your smartphone traffic, that doesn't mean that you can ignore the other 20%. As we've seen above, that 20% could be under-represented due to older devices failing to load your site <em>at all</em>.</p>
<h2 id="solutions" tabindex="-1">Solutions <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#solutions" aria-hidden="true">#</a></h2>
<p>Right, that's enough of problems. How do we go about getting the right information?</p>
<h3 id="analytics" tabindex="-1">Analytics <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#analytics" aria-hidden="true">#</a></h3>
<p>There's little that you can do to improve your analytics. With GA you can increase the site speed sample rate, but the data still doesn't have much value. It's the same with Adobe Analytics. Unfortunately I think it's necessary to separate performance and analytics products, even though they have a significant overlap. There's a niche waiting to be exploited! NCC's RUM solution has been designed to help build a business case for site speed improvements. I highly recommend a <a href="https://www.nccgroup.trust/rum-webinar/">three week free trial</a>.</p>
<h3 id="solving-phantom-bounces" tabindex="-1">Solving phantom bounces <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#solving-phantom-bounces" aria-hidden="true">#</a></h3>
<p>Phantom bounces are an interesting challenge. From the DoubleClick study we know that there is a disparity between the clicks being logged and the pages being loaded. The flow goes something like this, assuming a click from Google's SERP:</p>
<ol>
<li>User clicks a link (actually loads a Google link)</li>
<li>Google logs the click, redirects user to your site</li>
<li>User's browser makes a request for your site</li>
<li>Your site responds with some HTML</li>
<li>HTML, CSS, JavaScript are processed</li>
<li>Page loads</li>
<li>Analytics beacon fires</li>
</ol>
<p>Ideally, we would get data from the first step - how many people make the intention to load the page. This information is extremely hard to get, although an advertising provider should report on the number of clicks. If you have a good URL strategy then you might be able to do your own DoubleClick-style study, correlating ad clicks with pageviews.</p>
<p>It's also possible to correlate page requests with pageviews. Web server or CDN access logs can be compared against reported page impressions to determine the analytics loss. Further correlation can be run against the useragent of the requests to identify devices which are more underreported in analytics than others. I have seen loss rates around 20% before.</p>
<h3 id="better-device-representation" tabindex="-1">Better device representation <a class="direct-link" href="https://simonhearne.com/2017/analytics-lie/#better-device-representation" aria-hidden="true">#</a></h3>
<p>The homogenous 'iPhone' and 'iPad' issue can be solved pretty easily using <a href="https://web.wurfl.io/#wurfl-js">WURFL.js</a>. Simply instrument the JavaScript snippet on your site, integrate with GA and voila: iPhone and iPad models in your analytics.</p>
<p>Unless your website has a very specific audience, base your device testing on your target markets not your analytics. <a href="https://www.amazon.co.uk/Best-Sellers-Electronics-Mobile-Phones-Smartphones/zgbs/electronics/5362060031/ref=zg_bs_nav_ce_3_1340509031">Amazon's best sellers list</a> is usually a great place to start, bolt on the last 2.5 years of Apple and Samsung flagships, plus the Chinese upstarts OnePlus and Xiaomi, and you'll get reasonable coverage.</p>
<p>The top 12 devices should give you around 20% coverage of the whole smartphone market. You'll need 60 devices to get to 50%! Testing on the latest flagships should be a given, testing on the budget devices that make up around <a href="http://opensignal.com/reports/2015/08/android-fragmentation/">half of the market</a> is the next step to take.</p>
<p>Get in touch via <a href="mailto:simon@hearne.me">email</a> or <a href="https://twitter.com/simonhearne">twitter</a> if you need help getting better performance data, optimising your analytics configuration or setting up a device lab.</p>
How to Optimise CSS Image Sprites2017-01-20T00:00:00Zhttps://simonhearne.com/2017/optimising-css-image-sprites/<p>CSS image sprites can provide a performance benefit for most sites.
A sprite combines multiple small images into one file and CSS slices out the relevant sections of the image when they are required.
This reduces the number of requests required to render a page, potentially meaning that the page renders faster over HTTP/1.1.</p>
<p>The humble sprite can become critical to web performance. It is common to see the main website logo buried in the middle of image slider controls and social media icons.
Unfortunately, like all images, sprites can become bloated and slow.</p>
<p>Here are some top tips to improve your sprite's performance:</p>
<h2 id="remove-your-logo-image" tabindex="-1">Remove your logo image <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#remove-your-logo-image" aria-hidden="true">#</a></h2>
<p>Serve your logo image as a separate file, referenced in an <code><img></code> element.
This will be downloaded earlier than the sprite file (which is discovered late by the browser).</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Gs-svN2s1a-320.avif 320w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Gs-svN2s1a-320.webp 320w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Gs-svN2s1a-320.jpeg" width="320" height="220" /></picture><figcaption>Amazon used to put its logo in a sprite. Why?!</figcaption></figure>
<p>A secondary benefit is that the logo asset can be cached for a very long period,
unaffected by minor changes to the other icons in the sprite.</p>
<h2 id="remove-icons-that-are-available-in-utf-8" tabindex="-1">Remove icons that are available in UTF-8 <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#remove-icons-that-are-available-in-utf-8" aria-hidden="true">#</a></h2>
<p>It's common to see simple elements such as left and right arrows included in sprites.
Oftentimes there is a UTF-8 symbol which provides a suitable match. <a href="https://www.utf8icons.com/">UTF-8 Icons</a> provides a good reference.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/9DFlUu-dqB-574.avif 574w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/9DFlUu-dqB-574.webp 574w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/9DFlUu-dqB-574.jpeg" width="574" height="250" /></picture><figcaption>Some of the characters available for free in UTF-8</figcaption></figure>
<h2 id="use-css-filters" tabindex="-1">Use CSS filters <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#use-css-filters" aria-hidden="true">#</a></h2>
<p>Want to change the colour of an icon when someone hovers over it? Don't add another image to the sprite, use CSS filters to do it automatically.
A <code>:hover</code> rule like <code>filter:hue-rotate(90deg);</code> or <code>filter:invert(100%);</code> might just do the job!</p>
<h2 id="separate-icons-with-very-different-colours" tabindex="-1">Separate icons with very different colours <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#separate-icons-with-very-different-colours" aria-hidden="true">#</a></h2>
<p>PNG compression works best for a limited colour palette. If your sprite has images with complex colours, consider moving them to a separate image.
A good test of this is to save your image as a PNG8 and see if any of the colours are lost in the process.</p>
<h2 id="save-as-a-png8" tabindex="-1">Save as a PNG8 <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#save-as-a-png8" aria-hidden="true">#</a></h2>
<p>Most image editors will save PNG files that have transparency as PNG32 by default.
This allows for 24 bits of colour data (true colour = >16.7 million colours) as well as 256 levels of transparency.</p>
<p>PNG8 allows for 255 colours plus on/off transparency. This should be plenty for most sprites and significantly reduces file size.</p>
<p><a href="https://tinypng.com/">TinyPNG.com</a> is a good place to start optimising your sprite.</p>
<h2 id="remove-whitespace" tabindex="-1">Remove whitespace <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#remove-whitespace" aria-hidden="true">#</a></h2>
<p>Whitespace is totally unnecessary in a sprite, as the CSS is defined at a pixel level.
While extra whitespace will not have a significant impact on filesize, thanks to PNG compression, the memory footprint on the device will be increased.
In a world of £50 Android phones from Amazon, we have to be extremely conservative with memory usage.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/sz9LTLTMSG-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/sz9LTLTMSG-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/sz9LTLTMSG-600.jpeg" width="600" height="348" /></picture><figcaption>That's a lot of wasted memory</figcaption></figure>
<h2 id="keep-it-under-14kb" tabindex="-1">Keep it under 14kB <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#keep-it-under-14kb" aria-hidden="true">#</a></h2>
<p>On the web, a packet is around 1,500B of data (the <a href="https://en.m.wikipedia.org/wiki/Maximum_transmission_unit">maximum transmission unit</a>). In general you should send up to 10 packets in the first round trip of a TCP connection (the <a href="https://en.m.wikipedia.org/wiki/TCP_congestion_control#Congestion_window">TCP initial congestion window</a>).</p>
<p>Assuming you expect the image sprite to load early in the page load, it might be the first object in one of the six available TCP connections (in HTTP/1.1), and thus subject to the initial congestion window. Keeping it under 14kB means that the image can download in one network round-trip (assuming header data is under 1kB), reducing the effect of latency and packet loss.</p>
<h2 id="make-it-square" tabindex="-1">Make it square <a class="direct-link" href="https://simonhearne.com/2017/optimising-css-image-sprites/#make-it-square" aria-hidden="true">#</a></h2>
<p>This may be a micro-optimisation, but square images are more efficient than rectangles on memory-constrained devices.</p>
Three WebPerf Takeaways from Velocity Europe2016-11-08T13:08:00Zhttps://simonhearne.com/2016/three-takeaways-from-velocity/<h2 id="1-performance-kpis-are-broken" tabindex="-1">1) Performance KPIs are broken <a class="direct-link" href="https://simonhearne.com/2016/three-takeaways-from-velocity/#1-performance-kpis-are-broken" aria-hidden="true">#</a></h2>
<p>This was one of the themes that ran like an undercurrent through the whole conference, starting with <a href="https://twitter.com/souders">Steve Souders</a> at WebPerfDays talking about the death of window.onload for performance measurement. It's been known for a while that the onload event is flawed for measuring performance, but it has been universal across browsers so was the lowest friction measure. The problem with measuring onload is that the number can be <a href="http://www.leanblog.org/tag/gaming-the-numbers/">gamed</a>. Better numbers can be achieved by moving content around, while the measure can also be influenced by third-party content. The onload does not necessarily bare any relation to user experience, especially with JavaScript-driven websites.</p>
<p>There are a great many alternatives to the onload event for measuring performance. <a href="https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index">Speed Index</a> measures the average time at which parts of the page are displayed in synthetic tests. <a href="http://www.webperformance.io/custom-user-timings">User timings</a> can measure specific events such as time-to-first-interaction for real user monitoring. None of these are a silver bullet: Speed Index is impacted by cookie banners and modals, and is dependent on test speed and viewport size. User timings have to be carefully thought out and defined for specific page types, making them labour intensive.</p>
<blockquote>
<p>recommendations and statistics are objective measures of performance, but user perception is not mathematics</p>
</blockquote>
<p><a href="https://twitter.com/mishunov">Denys Mishunov</a> also spoke at WebPerfDays, he says that "recommendations and statistics are objective measures of performance, but user perception is not mathematics". Denys had some great examples of objectively fast experiences that had a poor user experience, such as a coin counting machine that users didn't trust because it <a href="http://www.90percentofeverything.com/2010/12/16/adding-delays-to-increase-perceived-value-does-it-work/">counted coins too quickly</a>. He extended this concept to the idea of passive- vs active-waiting: we can trick the user into thinking an interaction is just the right speed by manipulating the experience. <a href="https://www.smashingmagazine.com/2016/11/true-lies-of-optimistic-user-interfaces/">Optimistic UIs</a> are an example of using this in the wild. This concept makes measuring simple timing points of pages almost totally irrelevant, instead we need to measure when the user thought that the key event occurred, the delay until it actually occurred and the failure rate of the optimistic UI interaction.</p>
<p>One of the questions asked by the audience after <a href="https://twitter.com/__kb">Kevin Bowman</a>'s great talk about <a href="http://conferences.oreilly.com/velocity/devops-web-performance-eu/public/schedule/detail/53557">surviving big events</a> was 'what is the uptime SLA for Sky Betting'. Kevin's response was that they didn't have an official SLA, everyone just tries their best and they trust others to do the same. From my experience, having an SLA leads to gaming the numbers. This is especially true when there are incentives (i.e. a financial bonus) linked to the reported figure.</p>
<blockquote>
<p>Targets always result in gaming</p>
</blockquote>
<p>Gaming the numbers certainly is not restricted to web performance. The UK National Health System has been highlighted as an organisation where KPIs have led to poor standards of care, leading to <a href="http://www.nationalhealthexecutive.com/Health-Care-News/nhs-performance-management-putting-standards-of-care-at-risk">unnecessary patient deaths</a>, even though the care givers give great concern to the welfare of their patients. KPIs are easy for senior stakeholders to set and measure - on paper they are the ideal way to get visibility into performance. Unfortunately static goals are open to abuse through manipulating the numbers in any number of ways.</p>
<p>To improve performance we should enable teams to be successful, to measure their own performance and set their own definitions of success.</p>
<h2 id="2-team-success-business-success" tabindex="-1">2) Team success = business success <a class="direct-link" href="https://simonhearne.com/2016/three-takeaways-from-velocity/#2-team-success-business-success" aria-hidden="true">#</a></h2>
<p>This was a positive theme that ran through a lot of the talks at velocity. We must enable our teams to do their jobs, let them make their own decisions and remove micro-management.
A great example of this was given by Kevin from Sky Betting. In the run up to the biggest event in SkyBet's calendar, they knew it was going to be tough. Kevin even said "we knew we were going to run out of compute resource, we just didn't know where". Team dynamics are critical in order to survive an event where just minutes of downtime can cost £100,000s of customer bets. SkyBet has cross-team 'squads', each of which has a responsibility on the production system. These squads are self-managing, so when something goes wrong they just get on and fix it. There were some close calls during the big event, with the platform almost coming to a stand-still. Kevin believes that the trust and independence of the squads allowed the issues to be resolved as quickly as possible, without large-scale war rooms which would have wasted valuable time.</p>
<blockquote>
<p>Successful teams are trusted and manage themselves.</p>
</blockquote>
<p><a href="https://twitter.com/sarahjwells">Sarah Wells</a> from the FT emphasized the importance of enabling teams to clearly state their key goal(s). Without clear, agreed upon direction, teams will never realise their maximum potential. As part of this goal, teams at the FT are empowered to choose whatever tools and processes they think will be the best fit for their problems. In order to maintain supportability these teams are then responsible for supporting their chosen technologies 24x7, if they're not already covered by first-line support.</p>
<p><a href="https://twitter.com/funzoneq">Arnoud Vermeer</a> from Leaseweb described the journey from <a href="http://conferences.oreilly.com/velocity/devops-web-performance-eu/public/schedule/detail/52562">waterfall to agile developement</a>. One of the key concepts that Arnoud attributes to the success of their agile methodology is autonomous, empowered teams. Each team is responsible for one product. That responsibility extends from planning through development to in-live support. This enabled Leaseweb to produce predictable development, with priorities based on business value. The development roadmap is transparent to the whole company, with all employees involved in strategic direction.</p>
<p>To keep Leaseweb on track, there is a shared dashboard which shows the results of an anonymous post-sprint retrospective survey. This gives an early indication of when team dynamics begin to fail.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/QP-pXCMB-7-600.avif 600w, https://simonhearne.com/img/QP-pXCMB-7-900.avif 900w, https://simonhearne.com/img/QP-pXCMB-7-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/QP-pXCMB-7-600.webp 600w, https://simonhearne.com/img/QP-pXCMB-7-900.webp 900w, https://simonhearne.com/img/QP-pXCMB-7-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/QP-pXCMB-7-600.jpeg 600w, https://simonhearne.com/img/QP-pXCMB-7-900.jpeg 900w, https://simonhearne.com/img/QP-pXCMB-7-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/QP-pXCMB-7-600.jpeg" width="1200" height="670" /></picture><figcaption>Leaseweb's Retro Dashboard (<a href="http://cdn.oreillystatic.com/en/assets/1/event/167/The%20Anarchist%20Cookbook_%20DevOps%20and%20Agile%20recipes%20for%20blowing%20up%20the%20waterfall%20Presentation.pdf">source</a>)</figcaption></figure>
<p><a href="https://twitter.com/shinynew_oz">Astrid Atkinson</a> listed seven factors which lead to successful teams in her keynote, which by proxy lead to business success:</p>
<ul>
<li>Diversity is strength - only with a diverse set of personalities can you truly develop strong ideas</li>
<li>The best teams are inclusive - all ideas should be treated with respect and openness</li>
<li>Seek understanding, not skills - when hiring, look for candidates who will be a good personal fit</li>
<li>Give power away - empower teams to make the right (or wrong) choices</li>
<li>Create safety - there should be no fear of speaking out, even to the CEO of the company</li>
<li>Mission matters - the company, team and individual must all have a clear common mission</li>
</ul>
<h2 id="3-we-are-not-using-great-web-features" tabindex="-1">3) We are not using great Web features <a class="direct-link" href="https://simonhearne.com/2016/three-takeaways-from-velocity/#3-we-are-not-using-great-web-features" aria-hidden="true">#</a></h2>
<p>There are a number of great Web features that are available to us today, yet few websites are utilising them. One example is the challenge of under-utilised bandwidth: currently, there is a delay while the browser downloads and parses the HTML document, discovers a <code><link rel='stylesheet'></code> directive and requests the CSS asset(s) required to render the page.</p>
<p>This process takes time. This delay is unnecessary as the server already knows what assets are critical to render the page!</p>
<p>Enter <code><link rel='preload'></code>.</p>
<p>Preload is an HTML directive or HTTP header which hints to the browser that the linked asset should be pre-emptively loaded, as soon as possible. As an HTML directive it doesn't offer much of a performance benefit in the described scenario. As an HTTP header however, it can offer a performance gain: the browser is made aware of a critical asset before the HTML is even downloaded.</p>
<p>HTTP/2 takes this paradigm one step further. <a href="https://www.igvita.com/2013/06/12/innovating-with-http-2.0-server-push/">HTTP/2 Server Push</a> allows the server to actively push the asset to the browser. Cloudflare's CDN currently <a href="https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/">implements this automatically</a> when it sees the preload header.</p>
<p><a href="https://twitter.com/colinbendell">Colin Bendell</a> of Akamai spoke about the <a href="http://conferences.oreilly.com/velocity/devops-web-performance-eu/public/schedule/detail/53584">promise of Push</a>. Although there is still some work to do on mainstream support (test your browser using Colin's <a href="https://canipush.com/">canipush.com</a>), we can deploy Push now. Pushing CSS shows performance gains, while pushing anything else might actually make things worse. <code><link rel='preload'></code> may still be the best option for WebFonts, for example.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/x2GvRrx0QK-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/x2GvRrx0QK-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/x2GvRrx0QK-600.jpeg" width="600" height="224" /></picture><figcaption>Potential win area for Push</figcaption></figure>
<p>Another technology mentioned a lot, again, was Progressive Web Apps. The magical combination of <a href="https://developers.google.com/web/updates/2014/11/Support-for-installable-web-apps-with-webapp-manifest-in-chrome-38-for-Android">web app manifests</a>, <a href="https://jakearchibald.com/2014/using-serviceworker-today/">service worker</a> and <a href="https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/">web push notifications</a> which give us a near-native web app experience on Android devices. The fundamental technologies for PWAs have <a href="https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/">been around since mid-2015</a>, and yet adoption is extremely low. Only 0.5% of mobile pages tested by HTTP Archive include a <code>manifest</code> object.</p>
<p><a href="https://twitter.com/grigs">Jason Grigsby</a> gave an <a href="http://conferences.oreilly.com/velocity/devops-web-performance-eu/public/schedule/detail/54697">interesting keynote</a> about PWAs. He explained that every step towards a PWA <a href="https://cloudfour.com/thinks/progressive-web-apps-simply-make-sense/">makes sense on its own</a>. As such it's shocking that web app manifests, the first step towards PWAs, have such a low penetration.</p>
<p>From speaking with my clients, it seems that there is a hesitance to 'jump in to bed' with PWAs. There are questions around the lack of support on iPhones as well as the feared erosion of native application market share. However, as Jason said, each step makes sense on its own. I recommend that my clients start work towards PWAs with a web app manifest and start to investigate the potential of service worker.</p>
<h2 id="bonus-empathise-with-your-audience" tabindex="-1">BONUS - Empathise with your audience <a class="direct-link" href="https://simonhearne.com/2016/three-takeaways-from-velocity/#bonus-empathise-with-your-audience" aria-hidden="true">#</a></h2>
<p>One of the key differentiating factors for the presentations I attended was whether the speaker empathised with their audience or not. By that I mean there were some speakers who had clearly thought about the Velocity audience and showed empathy for their pains and challenges at the very beginning of their talks. There were other speakers who appeared to be speaking for their own benefit, and had not thought about why the audience might want to hear what they have to say.</p>
<p>I'll do my best to keep that in mind when speaking, I hope that others will do the same.</p>
Velocity NY 2016 - Wrap-up2016-10-11T14:29:00Zhttps://simonhearne.com/2016/velocity-ny-2016-wrap-up/<p>(p.s., the <a href="https://www.youtube.com/watch?v=__KEbUEFHpE">video of my talk</a> on performance for low powered devices is up!)</p>
<h2 id="real-user-monitoring-is-getting-exciting-but-has-gaps" tabindex="-1">Real User Monitoring is getting exciting, but has gaps <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#real-user-monitoring-is-getting-exciting-but-has-gaps" aria-hidden="true">#</a></h2>
<p>Everyone is using RUM. The vendors are maturing rapidly to be able to offer either:</p>
<ul>
<li>Single view of customer - integration with Application Performance Monitoring.</li>
<li>Insight - analysis of user behaviour and business events to create actionable insight.</li>
</ul>
<p>An example of a vendor going down the second route is SOASTA’s mPulse; there was a lot of discussion around <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51082">machine learning</a>. The key conclusions from <a href="https://twitter.com/tameverts">Tammy</a> and <a href="https://twitter.com/patmeenan">Pat</a>'s <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51082">talk</a> were that:</p>
<ul>
<li>the best predictor of bounce rate is DOM Ready time</li>
<li>the number of <code><script></code>s and DOM elements are inversely correlated with conversion rate</li>
</ul>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/-x4hgXXm0g-600.avif 600w, https://simonhearne.com/img/-x4hgXXm0g-900.avif 900w, https://simonhearne.com/img/-x4hgXXm0g-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/-x4hgXXm0g-600.webp 600w, https://simonhearne.com/img/-x4hgXXm0g-900.webp 900w, https://simonhearne.com/img/-x4hgXXm0g-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/-x4hgXXm0g-600.jpeg 600w, https://simonhearne.com/img/-x4hgXXm0g-900.jpeg 900w, https://simonhearne.com/img/-x4hgXXm0g-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/-x4hgXXm0g-600.jpeg" width="1200" height="733" /></picture><figcaption>Conversion likelihood by number of DOM nodes.</figcaption></figure>
<p>SOASTA also introduced <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/50522">measuring continuity</a>, using cool new script snippets to measure user behaviour. For example: measuring dead clicks, frame rate, time between intent and action. It’s all exciting stuff but I have concerns about the impact on user experience that these measurements will have (see <a href="http://www.dnb.com/perspectives/data-management-and-analytics/recognizing-observer-effect-issues-in-data-science.html">observer effect & bias</a>!). Their experiments are available in a public <a href="https://github.com/soasta/measuring-continuity">GitHub repo</a>.</p>
<p>There is clearly a lot of potential in using RUM to make predictions and measure outcomes. What we’re still missing is tight integration with experiment technology (e.g. Maxymiser or Google Experiments). Without that, we can’t use RUM to properly manage development decisions. <a href="https://twitter.com/mcmillanstu">Stuart McMillan</a> from Schuh recently mentioned that they use Google Analytics for performance data during experiments, probably because the integration works well. Unfortunately, the site speed data from Analytics is pretty poor… In the example below site speed metrics were only collected for under 1% of pageviews!</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/PP7-nMyo7T-600.avif 600w, https://simonhearne.com/img/PP7-nMyo7T-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/PP7-nMyo7T-600.webp 600w, https://simonhearne.com/img/PP7-nMyo7T-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/PP7-nMyo7T-600.jpeg 600w, https://simonhearne.com/img/PP7-nMyo7T-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/PP7-nMyo7T-600.jpeg" width="900" height="137" /></picture><figcaption>Screenshot from Google Analytics low sample rate for site speed metrics.</figcaption></figure>
<h2 id="synthetic-monitoring-vendors-are-disappearing-from-velocity" tabindex="-1">Synthetic Monitoring vendors are disappearing from Velocity <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#synthetic-monitoring-vendors-are-disappearing-from-velocity" aria-hidden="true">#</a></h2>
<p>There was virtually no presence from synthetic monitoring vendors at Velocity NY, the exception being <a href="https://twitter.com/mdaoudi">Mehdi</a> from <a href="http://www.catchpoint.com/">Catchpoint</a>. This is a trend that I’ve noticed over the past three years, probably driven by the commoditised synthetic monitoring being bundled into APM solutions (e.g. New Relic’s <a href="https://newrelic.com/synthetics">Synthetics</a>).</p>
<p>I think this says a lot about the advancement of RUM and APM. Synthetic gives the heartbeat monitoring and alerting for operational awareness, but if you know that your infrastructure and applications are ok, and that customers are having a good experience, then you don’t need synthetic monitoring to tell you a page is available.</p>
<h2 id="webpagetest-is-bigger-than-ever" tabindex="-1">WebPageTest is bigger than ever <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#webpagetest-is-bigger-than-ever" aria-hidden="true">#</a></h2>
<p>Almost every talk referenced <a href="http://www.webpagetest.org/">WebPageTest</a> or <a href="http://httparchive.org/">HTTPArchive</a> (which uses WebPageTest under the hood). It has certainly become the defacto web performance testing tool, with companies forming off the back of it such as <a href="https://speedcurve.com/">SpeedCurve</a>. What seems to be missing is contribution back in to the project. Looking at the contributors page on GitHub shows that <a href="https://twitter.com/patmeenan">Pat</a> is responsible for almost all development on the project.</p>
<p>Perhaps the industry (<a href="https://simonhearne.com/2015/find-third-party-assets/">including myself</a>) is taking this for granted.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/9oSjRrRAZh-600.avif 600w, https://simonhearne.com/img/9oSjRrRAZh-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/9oSjRrRAZh-600.webp 600w, https://simonhearne.com/img/9oSjRrRAZh-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/9oSjRrRAZh-600.jpeg 600w, https://simonhearne.com/img/9oSjRrRAZh-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/9oSjRrRAZh-600.jpeg" width="900" height="382" /></picture><figcaption>WebPageTest contibutors page on <a href="https://github.com/WPO-Foundation/webpagetest/graphs/contributors">GitHub</a> showing Pat's huge contribution.</figcaption></figure>
<h2 id="big-companies-are-talking-about-site-speed" tabindex="-1">Big companies are talking about site speed <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#big-companies-are-talking-about-site-speed" aria-hidden="true">#</a></h2>
<p>Ancestry and GoDaddy both spoke at Velocity. <a href="https://twitter.com/silentrant">Jed Wood</a>’s talk about <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51033">creating a performance culture</a> at Ancestry was insightful, describing the journey from quick wins (gzip, images etc.) to a full understanding of performance. To do this, Ancestry track business metrics such as conversions, alongside user-centric performance metrics such as time to an ancestor’s name rendering. The focus on both business metrics and user-centric metrics means that Jed can demonstrate a correlation to the business to help drive further work on site speed.</p>
<p>Improving the <a href="http://ancestry.com/">Ancestry.com</a> sign up page from 2.7 seconds to 1.7 generated a 7% increase in conversions. Interestingly, further work to get to 1.3 seconds made no further improvement to conversion. The chart below is taken from SpeedCurve and shows the team's progress over time to reduce Speed Index. What I really like about this is the use of annotations to mark where changes and releases occurred, so changes in performance can be traced back to a specific build of the website.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/SuhLEno2Ud-600.avif 600w, https://simonhearne.com/img/SuhLEno2Ud-900.avif 900w, https://simonhearne.com/img/SuhLEno2Ud-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/SuhLEno2Ud-600.webp 600w, https://simonhearne.com/img/SuhLEno2Ud-900.webp 900w, https://simonhearne.com/img/SuhLEno2Ud-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/SuhLEno2Ud-600.jpeg 600w, https://simonhearne.com/img/SuhLEno2Ud-900.jpeg 900w, https://simonhearne.com/img/SuhLEno2Ud-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/SuhLEno2Ud-600.jpeg" width="1200" height="656" /></picture><figcaption>Screenshot of Ancestry.com's SpeedCurve Speed Index dashboard.</figcaption></figure>
<p>One of the ways Ancestry maintains performance is by adding an artificial delay to third-party scripts executing. I wonder if <a href="https://developers.google.com/web/updates/2015/08/using-requestidlecallback">requestIdleCallback</a> could be used for this?</p>
<p><a href="https://twitter.com/perfmangodaddy">Jim Pierson</a> from GoDaddy took a different approach. To prove that performance was important, he and an engineer <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/50588">secretly improved the performance</a> of the GoDaddy homepage in India, while there was no change in marketing activity. The result of improving load time by 50% was an additional $35,000 of revenue per day. Now if that doesn’t sell the value of performance I’m not sure what will. Those performance tweaks are now rolled out across all of GoDaddy.</p>
<p>Jim used a maturity model to describe his journey in web performance, with anomaly detection, regression analysis and communication being at the top. I think the most important point that Jim made was the need for solid understanding of performance impact across the business. There's nothing quite like $35,000 to do that, I suppose.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/JYDg7Wlpga-600.avif 600w, https://simonhearne.com/img/JYDg7Wlpga-900.avif 900w, https://simonhearne.com/img/JYDg7Wlpga-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/JYDg7Wlpga-600.webp 600w, https://simonhearne.com/img/JYDg7Wlpga-900.webp 900w, https://simonhearne.com/img/JYDg7Wlpga-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/JYDg7Wlpga-600.jpeg 600w, https://simonhearne.com/img/JYDg7Wlpga-900.jpeg 900w, https://simonhearne.com/img/JYDg7Wlpga-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/JYDg7Wlpga-600.jpeg" width="1200" height="514" /></picture><figcaption>Jim Pierson's web performance maturity pyramid.</figcaption></figure>
<h2 id="single-page-apps-are-slow" tabindex="-1">Single Page Apps are slow <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#single-page-apps-are-slow" aria-hidden="true">#</a></h2>
<p>I was really happy that someone else said this out loud. <a href="https://twitter.com/livshitz98">Boris</a> and <a href="https://twitter.com/MD_A13">Manuel</a> from Akamai talked about <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51232">making SPAs faster</a> through selecting the right SPA framework, using JS bundlers, server-side rendering and tricking the user with a skeleton page. All of these are hacks around the fundamental problem with client-side applications. As such, I’m not a fan!</p>
<p>I also spoke a lot about SPAs being slow <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51254">in my presentation</a>. In my study, a SPA will generally be 43% slower than a traditional web page. Of course this difference is magnified on mobile devices.</p>
<h2 id="amp-is-not-the-killer-feature" tabindex="-1">AMP is not the killer feature <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#amp-is-not-the-killer-feature" aria-hidden="true">#</a></h2>
<p>The <a href="https://www.ampproject.org/">Accelerated Mobile Pages</a> project is over a year old now. There were two talks about AMP which took very different approaches. <a href="https://twitter.com/cramforce">Malte Ubl</a> (creator of AMP) <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/50798">gave a talk</a> about the current state of AMP and what’s coming in the future, while <a href="https://twitter.com/nicj">Nic</a> and <a href="https://twitter.com/querymetrics">Nigel</a> of SOASTA used analytics data gathered by mPulse to <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51319">describe what consumers were doing with AMP pages</a>.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/U4tnUs38Ob-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/U4tnUs38Ob-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/U4tnUs38Ob-600.jpeg" width="600" height="316" /></picture><figcaption>Misleading statistic from the <a href="https://amphtml.wordpress.com/2016/10/07/amp-a-year-in-review/">AMP Year in Review</a>.</figcaption></figure>
<p>One of the interesting points brought up by Malte was that as AMP pages are almost always (>99%) served by the AMP CDN, there is a lot of potential for static performance optimisation. For example, the average image on the AMP CDN can be further compressed to reduce the file size by 40%. Rolling out these optimisations at the CDN level will have a great bang-for-buck across all AMP pages.</p>
<p>Analytics gathered by SOASTA paint a rather gloomy picture for publishers using AMP. While AMP pages are almost six times faster than the regular page, they take users out of the publishers’ domain. The probability of a reader of an AMP article going to the <em>article publishers’ own site</em> in the next 30 days is only 3%. So it seems there is a significant trade-off to be had: in order to have super-fast articles that are <em>promoted by Google in search results</em>, you have to sacrifice engagement and brand awareness.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/i_MVujkVwU-600.avif 600w, https://simonhearne.com/img/i_MVujkVwU-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/i_MVujkVwU-600.webp 600w, https://simonhearne.com/img/i_MVujkVwU-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/i_MVujkVwU-600.jpeg 600w, https://simonhearne.com/img/i_MVujkVwU-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/i_MVujkVwU-600.jpeg" width="900" height="500" /></picture><figcaption>Scary engagement statistic for publishers.</figcaption></figure>
<p>This all feels very walled-garden, especially as AMP pages are just optimised web pages, which anyone can make. I like the fact that it promotes fast content as better content, but I don’t think it’s in the publisher’s best interest. <a href="https://twitter.com/tkadlec">Tim Kadlec</a> has proposed an open alternative to AMP, the <a href="https://timkadlec.com/2016/02/a-standardized-alternative-to-amp/">Content Performance Policy</a>, which is definitely worth read.</p>
<h2 id="there-are-lots-of-underutilised-performance-and-security-features-on-the-web" tabindex="-1">There are lots of underutilised performance and security features on the web <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#there-are-lots-of-underutilised-performance-and-security-features-on-the-web" aria-hidden="true">#</a></h2>
<p><a href="https://twitter.com/soniaburney">Sonia</a> and <a href="https://twitter.com/sabrina_burney">Sabrina</a> Burney of Akamai promised a talk on the <a href="http://conferences.oreilly.com/velocity/devops-web-performance-ny/public/schedule/detail/51203">cross-overs in web security and web performance</a>. The talk was fast-paced and covered a lot of ground including how features we use already (iframes, pre-load etc.) have additional security options that few people use. This was the most practical session of the conference for me, and I need to list all of these out to make sense of it:</p>
<h3 id="iframes" tabindex="-1">iFrames <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#iframes" aria-hidden="true">#</a></h3>
<p>Iframes are great for performance, kinda.
Over 60% of sites use iframes, yet virtually none of them use the <a href="https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/">sandbox attribute</a>. Sandbox allows you to define what access the iframe has to the parent page and whether it can execute scripts. It will also deny pointer lock, form submissions and a whole host of other scary stuff.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/xHZ3rbTT2N-600.avif 600w, https://simonhearne.com/img/xHZ3rbTT2N-900.avif 900w, https://simonhearne.com/img/xHZ3rbTT2N-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/xHZ3rbTT2N-600.webp 600w, https://simonhearne.com/img/xHZ3rbTT2N-900.webp 900w, https://simonhearne.com/img/xHZ3rbTT2N-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/xHZ3rbTT2N-600.jpeg 600w, https://simonhearne.com/img/xHZ3rbTT2N-900.jpeg 900w, https://simonhearne.com/img/xHZ3rbTT2N-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/xHZ3rbTT2N-600.jpeg" width="1200" height="169" /></picture><figcaption>Using sandbox with iframes.</figcaption></figure>
<h3 id="prefetch-preload-as" tabindex="-1">Prefetch / Preload <code>As</code> <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#prefetch-preload-as" aria-hidden="true">#</a></h3>
<p>Prefetch allows web developers to provide hints to browsers about assets which should be optimistically downloaded. This is great for objects which are critical to render, such as CSS and WebFonts. Prefetch can be used in <link /> tags or in HTTP headers, the advantage of a header being that it can be used by the browser before the HTML document has been downloaded and parsed.</p>
<p>Preload builds on prefetch by forcing the browser to download the asset, whereas prefetch hints can be ignored.</p>
<p>Both of these have an additional optional attribute: <code>as</code>. This allows us to define the type of asset to be loaded, e.g. 'image', 'script' or any one of the <a href="https://fetch.spec.whatwg.org/#concept-request-destination">standard fetch types</a>. Adding the type of asset allows the browser to send the correct Accept header, as well as ensuring that any content security policy can be applied correctly to the preloaded asset.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/rGHCaVCxEB-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/rGHCaVCxEB-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/rGHCaVCxEB-600.jpeg" width="600" height="179" /></picture><figcaption>Using prefetch and preload with `as` attribute.</figcaption></figure>
<h3 id="content-security-policy" tabindex="-1">Content Security Policy <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#content-security-policy" aria-hidden="true">#</a></h3>
<p>Speaking of content security policy... This is the header directive which tells a browser what permissions each domain used on a site has. For example, <a href="http://fonts.example.com/">fonts.example.com</a> should not be able to execute scritps, and <a href="http://static.example.com/">static.example.com</a> might only be for serving images. Using a <a href="https://developers.google.com/web/fundamentals/security/csp/">content security policy</a> ensures that only the correct domains have access to the browser, which is obviously a security win. The performance benefit comes mainly from the audit and analysis required in order to write a CSP - what domains are being used on our site, for what, and why?!</p>
<pre class="language-http"><code class="language-http">Content-Security-Policy:<br /> default-src 'self' ;<br /> img-src 'self' https://*.google.com ;<br /> script-src 'self' http://www.google-analytics.com 'unsafe-inline'<br /> http://*gstatic.com ;<br /> style-src 'self' https://fonts.googleapis.com ;<br /> font-src 'self' https://themes.googleusercontent.com ;<br /> frame-src 'self' http://3rdparty.com<br /> sandbox 'allow-scripts';</code></pre>
<h3 id="service-worker" tabindex="-1">Service Worker <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#service-worker" aria-hidden="true">#</a></h3>
<p>Now we come to service worker, the magic JavaScript proxy thread. This is the concept most fundamental to 'offline-first': a developer-controlled proxy which can intercept network requests, build an internal cache and do all sorts of other magic.</p>
<p>For performance, service worker is critical for good performance in poor network conditions, if you haven't seen <a href="https://twitter.com/jaffathecake">Jake Archibald</a> talk about service worker yet, check out his <a href="https://www.youtube.com/watch?v=cmGr0RszHc8">video from Google IO</a>. No, seriously, go watch that video.</p>
<p>Now on to security. As service worker has access to network requests, it can be used to enforce rules in a similar way to content security policy. You can even maintain your own black- and white-lists of blocked assets or domains, and let the service worker manage how these are enforced in the browser. Cool, eh?</p>
<p>I think the next logical step is for service workers to apply time thresholds to third-party downloads. That tracker tag taking over a second to load? Kill the request, give a safe response to the browser, and log the event to keep track of how often it happens. The possibilities are almost endless!</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/PsZe666mbe-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/PsZe666mbe-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/PsZe666mbe-600.jpeg" width="600" height="290" /></picture><figcaption>Using service worker as a client reputation enforcer.</figcaption></figure>
<h3 id="sub-resource-integrity" tabindex="-1">Sub-Resource Integrity <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#sub-resource-integrity" aria-hidden="true">#</a></h3>
<p>Sabrina and Sonia only briefly mentioned SRI but I think it's worth covering in a little more detail here. SRI allows you to take a hash of an asset (e.g. jQuery v1.9.1, minified) and add that to your <code><script></code> tag. If the downlaoded asset does not have the same hash, then the browser will refuse to execute it. This is really handy for third-party content that has the potential to effect user experience and/or security, or whose provenance is not entirely clear (JavaScript CDNs, anyone?)</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/example-framework.js<span class="token punctuation">"</span></span><br /> <span class="token attr-name">integrity</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC<span class="token punctuation">"</span></span><br /> <span class="token attr-name">crossorigin</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>anonymous<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span></code></pre>
<h2 id="progressive-web-apps-aren-t-all-that-yet" tabindex="-1">Progressive web apps aren't all that. Yet. <a class="direct-link" href="https://simonhearne.com/2016/velocity-ny-2016-wrap-up/#progressive-web-apps-aren-t-all-that-yet" aria-hidden="true">#</a></h2>
<p>There was a lot of conversation around <a href="https://developers.google.com/web/progressive-web-apps/">progressive web applications</a>, but the lack of good browser support for some of the key features means that large organisations are holding off implementation: Web Push is only <a href="http://caniuse.com/#feat=push-api">available on Firefox and Chrome</a>, service worker is only <a href="http://caniuse.com/#feat=serviceworkers">partially supported on Firefox, Chrome and Opera</a>.</p>
<p>I'm also wary of websites going fully PWA - which may not provide the desktop with a good experience. Definitely a 'watch this space'.</p>
<p>In summary, an awesome conference with huge amounts to mull over.</p>
Blocking requests in webpagetest? Don't use Chrome.2016-07-11T14:01:00Zhttps://simonhearne.com/2016/blocking-requests-in-webpagetest-dont-use-chrome/<blockquote class="info"><p>Note: this issue has since been resolved by the Chrome team, please proceed with caution!</p></blockquote>
<p>TL;DR: Blocking third-party requests in webpagetest shows you the impact they're having on a page, but Chrome is inefficient at blocking the requests. So use another browser such as Internet Explorer to do third-party impact testing.</p>
<p>In order to demonstrate the performance impact that third-party content can have, I like to remove the offending requests to show a before & after view of a web page. Nothing says 'your third-parties are harming user experience' quite like a video of a page loading twice as fast without them.</p>
<p>To do this, I run a test in webpagetest and get the list of domains as JSON. I parse this to filter out first- & second-party domains then re-run the test with all of the third-party domains blocked by webpagetest. Often, though, just blocking tag container(s) will do the job.</p>
<p>Let's take an example: <a href="http://www.cyclingweekly.co.uk/">cyclingweekly.co.uk</a>. Not for any reason other than I like bikes. When you load the homepage, approximately 150 objects are loaded from 60 domains:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Ws2LWtjCVz-600.avif 600w, https://simonhearne.com/img/Ws2LWtjCVz-900.avif 900w, https://simonhearne.com/img/Ws2LWtjCVz-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Ws2LWtjCVz-600.webp 600w, https://simonhearne.com/img/Ws2LWtjCVz-900.webp 900w, https://simonhearne.com/img/Ws2LWtjCVz-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/Ws2LWtjCVz-600.jpeg 600w, https://simonhearne.com/img/Ws2LWtjCVz-900.jpeg 900w, https://simonhearne.com/img/Ws2LWtjCVz-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Ws2LWtjCVz-600.jpeg" width="1200" height="690" /></picture><figcaption>Requestmap of cyclingweekly.co.uk.</figcaption></figure>
<p>Now to test the impact of this large number of third-parties lets block them and re-run the test. Here's the result:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/JIbqyqudYA-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/JIbqyqudYA-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/JIbqyqudYA-600.jpeg" width="600" height="112" /></picture><figcaption>Filmstrip comparison without & with third-party content.</figcaption></figure>
<p>Kind of what I expected, the third-parties mean the page takes longer to finish. But I had secretly hoped that they were also affecting render performance - that the blocked version would render quicker.</p>
<p>To dig in a bit deeper I looked at the waterfall chart of the blocked version, and I noticed two things: the requests seemed to be sequential rather than parallel, and the CPU was pegged at 100% for most of the time. Curious.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/r4Z_0i4Iw_-600.avif 600w, https://simonhearne.com/img/r4Z_0i4Iw_-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/r4Z_0i4Iw_-600.webp 600w, https://simonhearne.com/img/r4Z_0i4Iw_-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/r4Z_0i4Iw_-600.jpeg 600w, https://simonhearne.com/img/r4Z_0i4Iw_-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/r4Z_0i4Iw_-600.jpeg" width="900" height="482" /></picture><figcaption>Waterfall chart showing sequential requests when third-parties are blocked</figcaption></figure>
<p>Whenever the CPU is pegged in a Chrome webpagetest result, here's a pro-tip: enable timeline capture.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/6kMV9A7BLl-600.avif 600w, https://simonhearne.com/img/6kMV9A7BLl-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/6kMV9A7BLl-600.webp 600w, https://simonhearne.com/img/6kMV9A7BLl-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/6kMV9A7BLl-600.jpeg 600w, https://simonhearne.com/img/6kMV9A7BLl-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/6kMV9A7BLl-600.jpeg" width="900" height="318" /></picture><figcaption>Enabling timeline capture in webpagetest</figcaption></figure>
<p>This gives you a json file that you can drag into your local Chrome developer tools window:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Fv-t2CmLYE-600.avif 600w, https://simonhearne.com/img/Fv-t2CmLYE-900.avif 900w, https://simonhearne.com/img/Fv-t2CmLYE-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Fv-t2CmLYE-600.webp 600w, https://simonhearne.com/img/Fv-t2CmLYE-900.webp 900w, https://simonhearne.com/img/Fv-t2CmLYE-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/Fv-t2CmLYE-600.jpeg 600w, https://simonhearne.com/img/Fv-t2CmLYE-900.jpeg 900w, https://simonhearne.com/img/Fv-t2CmLYE-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Fv-t2CmLYE-600.jpeg" width="1200" height="432" /></picture><figcaption>Download timeline json.</figcaption></figure>
<p>Loading this up in Chrome Canary lets you analyse the network waterfall in the timeline view, where I saw significant gaps in the processing timeline, and a number of grey requests:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/-KWO4K4Klw-600.avif 600w, https://simonhearne.com/img/-KWO4K4Klw-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/-KWO4K4Klw-600.webp 600w, https://simonhearne.com/img/-KWO4K4Klw-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/-KWO4K4Klw-600.jpeg 600w, https://simonhearne.com/img/-KWO4K4Klw-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/-KWO4K4Klw-600.jpeg" width="900" height="262" /></picture><figcaption>Blocked requests appearing and gaps in timeline.</figcaption></figure>
<p>The grey request was one which should have been blocked by webpagetest. You can't drill into any more details from this view, although it looks like the blocked requests still consume time yet don't have a response.</p>
<p>In <a href="https://github.com/WPO-Foundation/webpagetest/issues/584">issue 584</a> on the webpagetest GitHub repo Pat Meenan states:</p>
<blockquote>
<p>blocking (or modifying request headers) in Chrome is exceedingly expensive</p>
</blockquote>
<p>So, trying again in Firefox and Internet Explorer we get the following results:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/ylNk_rfoC2-600.avif 600w, https://simonhearne.com/img/ylNk_rfoC2-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ylNk_rfoC2-600.webp 600w, https://simonhearne.com/img/ylNk_rfoC2-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ylNk_rfoC2-600.jpeg 600w, https://simonhearne.com/img/ylNk_rfoC2-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/ylNk_rfoC2-600.jpeg" width="900" height="592" /></picture><figcaption>Waterfall charts for three browsers while blocking third-party domains.</figcaption></figure>
<p>And for visual performance, IE gives the best result:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/I9Gs-p1G0W-600.avif 600w, https://simonhearne.com/img/I9Gs-p1G0W-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/I9Gs-p1G0W-600.webp 600w, https://simonhearne.com/img/I9Gs-p1G0W-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/I9Gs-p1G0W-600.jpeg 600w, https://simonhearne.com/img/I9Gs-p1G0W-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/I9Gs-p1G0W-600.jpeg" width="900" height="502" /></picture><figcaption>Waterfall charts for three browsers while blocking third-party domain.</figcaption></figure>
<p>So the final result, to show the impact of third-party content on the user experience comes out below:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/blocking-requests/cyclingweekly_ieblocked2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/kT5kzut14H-600.avif 600w, https://simonhearne.com/img/kT5kzut14H-900.avif 900w, https://simonhearne.com/img/kT5kzut14H-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/kT5kzut14H-600.webp 600w, https://simonhearne.com/img/kT5kzut14H-900.webp 900w, https://simonhearne.com/img/kT5kzut14H-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/kT5kzut14H-600.jpeg 600w, https://simonhearne.com/img/kT5kzut14H-900.jpeg 900w, https://simonhearne.com/img/kT5kzut14H-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/kT5kzut14H-600.jpeg" width="1200" height="81" /></picture></a><figcaption>Filmstrip image showing with cyclingweekly.co.uk loading without & with third-party assets.</figcaption></figure>
How fast is fast enough?2016-06-29T21:53:00Zhttps://simonhearne.com/2016/how-fast-is-fast-enough/<p>Do you know when your website's speed starts to impact your customers' behaviour?</p>
<p>The speed of your site affects how your visitors behave: how many pages they view, how much they spend or even whether they convert.</p>
<p>Join me and one of the world's leading web performance experts, Andy Davies, as we discuss what you should consider when measuring your site's speed and how to determine when speed is a problem.</p>
<p>You'll leave this webinar with a good understanding of why speed matters, what good performance looks like and how it impacts your business.</p>
<p>We're really lucky to have some concrete RUM data and experience to work out what's important. The webinar has been and gone, but you can see the slides below.</p>
<br />
<script async="" class="speakerdeck-embed" data-id="ebcabd2299de462dbd6dbc47c6b8c7a3" data-ratio="1.77777777777778" src="https://speakerdeck.com/assets/embed.js"></script>
Getting Reliable Visual Performance Metrics2016-06-08T11:26:00Zhttps://simonhearne.com/2016/getting-reliable-visual-metrics/<p>Visual metrics such as Speed Index, render start and visual complete are the best proxies we have for user experience. Unfortunately two of these are totally thrown by rotating carousels, popups, cookie banners and adverts.</p>
<p>To give an example, below is the homepage of Transport for London loading at Cable speed from London. It's clear that the main content of the page is visible by 6.5 seconds. The metrics we get from WebPageTest don't tally though, with visual complete coming in at almost 12 seconds.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/c1yORZX1j--600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/c1yORZX1j--600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrip section showing TfL appear visually complete at 6.5 seconds." loading="lazy" decoding="async" src="https://simonhearne.com/img/c1yORZX1j--600.jpeg" width="600" height="128" /></picture><figcaption>Filmstrip section showing TfL appear visually complete at 6.5 seconds.</figcaption></figure>
<p>The reason that the WebPageTest metrics disagree with what we can see is that a few seconds after the 'first' visual complete, an advert appears which pushes content down the page. Notice that in the image above the visual completeness is reported at 90%. The image below shows what happens between 9 and 12 seconds.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/ZRT5XbMTuF-600.avif 600w, https://simonhearne.com/img/ZRT5XbMTuF-900.avif 900w, https://simonhearne.com/img/ZRT5XbMTuF-1200.avif 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/ZRT5XbMTuF-600.webp 600w, https://simonhearne.com/img/ZRT5XbMTuF-900.webp 900w, https://simonhearne.com/img/ZRT5XbMTuF-1200.webp 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/ZRT5XbMTuF-600.jpeg 600w, https://simonhearne.com/img/ZRT5XbMTuF-900.jpeg 900w, https://simonhearne.com/img/ZRT5XbMTuF-1200.jpeg 1200w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrip section showing TfL visually complete at 12 seconds." loading="lazy" decoding="async" src="https://simonhearne.com/img/ZRT5XbMTuF-600.jpeg" width="1200" height="145" /></picture><figcaption>Filmstrip section showing TfL visually complete at 12 seconds.</figcaption></figure>
<p>This effect can be caused by a number of issues:</p>
<ul>
<li>Cookie banners which appear after the page loads</li>
<li>Adverts which load late</li>
<li>Carousels which auto-advance</li>
<li>Auto-play videos</li>
<li>Subscription popups</li>
</ul>
<p>Unfortunately this is normally outside of my control as a consultant. It is an especially pertinent issue when I'm comparing multiple websites or pages, as some can appear artificially worse than others.</p>
<p>While I believe that anything which creates this effect is bad for user experience (e.g. <a href="http://shouldiuseacarousel.com/">rotating carousels</a>), it's not my place to make recommendations on their use. So, we need a way to get reliable visual metrics given that a page under test can have one or more of these features.</p>
<p>When testing a page, WebPageTest has a default activity threshold of 2 seconds. That is: after all page activity stops, the test will continue for a further 2 seconds. If there is network activity within this time the timeout will reset. If the screen changes within this time, the final frame (after the timeout) will be used as the reference point for visually complete and Speed Index.</p>
<p>Thus, a potential way to improve visual metrics is to reduce this timeout. To do so in WebPageTest you can use a script, e.g.:</p>
<pre class="language-text"><code class="language-text">setActivityTimeout 100<br />navigate http://tfl.gov.uk</code></pre>
<p>The result is a page which finishes before the advert pushes content down, as shown below.</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/GhA-OyxYw4-600.avif 600w, https://simonhearne.com/img/GhA-OyxYw4-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/GhA-OyxYw4-600.webp 600w, https://simonhearne.com/img/GhA-OyxYw4-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/GhA-OyxYw4-600.jpeg 600w, https://simonhearne.com/img/GhA-OyxYw4-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Filmstrips of TfL tested with and without a 100ms activity timeout." loading="lazy" decoding="async" src="https://simonhearne.com/img/GhA-OyxYw4-600.jpeg" width="900" height="342" /></picture><figcaption>Filmstrips of TfL tested with and without a 100ms activity timeout.</figcaption></figure>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/dn7rM489W2-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/dn7rM489W2-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Visual progression over time." loading="lazy" decoding="async" src="https://simonhearne.com/img/dn7rM489W2-600.jpeg" width="600" height="406" /></picture><figcaption>Visual progression over time.</figcaption></figure>
<p>So if you want more reliable visual performance metrics, reduce the activity timeout threshold to something reasonable - like 100ms.</p>
<p>Unfortunately this can only be done through scripted sessions, or by changing the <a href="https://github.com/WPO-Foundation/webpagetest/blob/master/agent/browser/ie/pagetest/PagetestBase.h">default in WebPageTest private</a> as shown below.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/* PagetestBase.h */</span><br />#define <span class="token constant">ACTIVITY_TIMEOUT</span> <span class="token number">2000</span><br />#define <span class="token constant">REQUEST_ACTIVITY_TIMEOUT</span> <span class="token number">2000</span><br />#define <span class="token constant">FORCE_ACTIVITY_TIMEOUT</span> <span class="token number">2000</span><br />#define <span class="token constant">DOC_TIMEOUT</span> <span class="token number">2000</span></code></pre>
Render Conf 20162016-01-21T21:54:00Zhttps://simonhearne.com/2016/render-conf-1/<h3 id="bruce-lawson-web-next" tabindex="-1"><a href="https://twitter.com/@brucel">Bruce Lawson</a> - web.next <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#bruce-lawson-web-next" aria-hidden="true">#</a></h3>
<h4 id="description-slides-video" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#webnext">Description</a> | <a href="https://speakerdeck.com/brucel/web-dot-next-render">Slides</a> | <a href="https://vimeo.com/album/3953264/video/165995132">Video</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-slides-video" aria-hidden="true">#</a></h4>
<p>I had assumed that Bruce's talk would be identical to the <a href="https://simonhearne.com/2015/velocity-europe-2015-report-1/#ensuring-a-high-performing-web-for-the-next-billion-people---bruce-lawson-opera-asa">last few I've seen</a>. Bruce's previous talks on web.next have focused on where customers are coming from and what devices they are using. This talk was much more focused around the web technologies that will <em>allow</em> the next 4 billion people to use the web.</p>
<p>Progressive Web Apps (PWAs) <a href="http://loxima.com/blog/the-death-of-apps/">will replace native mobile apps</a> and this is a good thing: a relatively light-weight 20MB app might take over 30 minutes to install on 2G - by which time the network will probably have flaked out, Bruce notes.</p>
<p>PWAs increase accessibility to mobile users with limited devices and limited connectivity.</p>
<p>In telling his story, Bruce highlighted how browser manufacturers have had to collaborate to create better web technologies - and thus better user experiences. Mozilla, Google, Opera and Microsoft are trying to improve their relationship with web developers to produce scalable and useful functionality. AppCache is the counter-example Bruce uses to demonstrate what happens when this relationship is not built correctly.</p>
<p>Another great talk by Bruce, inspiring a room of front-end engineers to get involved with the Web Platform Incubator Group (<a href="https://simonhearne.com/2016/render-conf-1/WICG">https://www.w3.org/blog/2015/07/wicg/</a>).</p>
<hr />
<h3 id="val-head-designing-meaningful-animation" tabindex="-1"><a href="https://twitter.com/@vlh">Val Head</a> - Designing Meaningful Animation <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#val-head-designing-meaningful-animation" aria-hidden="true">#</a></h3>
<h4 id="description-video" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#designing-meaningful-animation">Description</a> | <a href="https://vimeo.com/album/3953264/video/165995133">Video</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-video" aria-hidden="true">#</a></h4>
<p>Val believes that animation is often mis-used and mis-understood on the web. Throughout the talk Val uses a mocked-up registration confirmation form to demonstrate how animation can be used to reinforce a brand message.</p>
<p>Val references Disney's <a href="https://en.wikipedia.org/wiki/12_basic_principles_of_animation">12 basic principles of animation</a> and highlights the key factors to focus on when animating on the web:</p>
<ul>
<li>Staging</li>
<li>Follow Through</li>
<li>Timing</li>
<li>Secondary Action</li>
</ul>
<p>Using a live example and the Chrome Developer Tools <a href="http://valhead.com/2015/01/06/quick-tip-chrome-animation-controls/">animation controls</a> Val shows that animations can convey meaning and emotion when these four factors are considered properly.</p>
<hr />
<h3 id="alicia-sedlock-the-landscape-of-front-end-testing" tabindex="-1"><a href="https://twitter.com/@aliciability">Alicia Sedlock</a> - The landscape of front-end testing <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#alicia-sedlock-the-landscape-of-front-end-testing" aria-hidden="true">#</a></h3>
<h4 id="description-slides-video-1" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#the-landscape-of-front-end-testing">Description</a> | <a href="https://speakerdeck.com/aliciasedlock/the-landscape-of-front-end-testing">Slides</a> | <a href="https://vimeo.com/album/3953264/video/165995134">Video</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-slides-video-1" aria-hidden="true">#</a></h4>
<p>Alicia knows that testing is hard, and that front-end developers aren't traditionally great at testing. By introducing testing framework language Alicia sets the scene for unit testing, introducing core concepts for front-end developers. The focus is on automation, making testing as simple as possible to drive adoption and engagement.</p>
<p>The testing concept then gets extended to visual regression testing, where Alicia lists the following automation tools to make this simple:</p>
<ul>
<li><a href="https://github.com/Huddle/PhantomCSS">PhantomCSS</a></li>
<li><a href="https://github.com/garris/BackstopJS">BackstopJS</a></li>
<li><a href="https://github.com/BBC-News/wraith">Wraith</a></li>
<li><a href="http://webdriver.io/">WebDriver.io</a></li>
<li><a href="https://percy.io/">Percy.io</a></li>
</ul>
<p>Of course no talk on testing would be complete without a nod to accessibility. Alicia lists two tools to help with automating accessibility testing:</p>
<ul>
<li><a href="https://github.com/addyosmani/a11y">a11y</a> / <a href="https://github.com/lucalanca/grunt-a11y">grunt-a11y</a></li>
<li><a href="http://pa11y.org/">pa11y</a></li>
</ul>
<p>And the final chapter in automated testing is performance (yay!). Alicia mentions a number of tools for automated testing:</p>
<ul>
<li><a href="https://github.com/tkadlec/grunt-perfbudget">grunt-perfbudget</a></li>
<li><a href="https://github.com/sindresorhus/gulp-size">gulp-size</a></li>
<li><a href="https://gist.github.com/tkadlec/7e352b74b1961a3e36d7">perf.js</a></li>
</ul>
<p>I would add to the list a notable exception:</p>
<ul>
<li><a href="https://www.sitespeed.io/">SiteSpeed.io</a></li>
</ul>
<hr />
<h3 id="harry-roberts-css-for-software-engineers-for-css-developers" tabindex="-1"><a href="https://twitter.com/@csswizardry">Harry Roberts</a> - CSS for software engineers for CSS developers <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#harry-roberts-css-for-software-engineers-for-css-developers" aria-hidden="true">#</a></h3>
<h4 id="description-slides-video-2" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#css-for-software-engineers-for-css-developers">Description</a> | <a href="https://speakerdeck.com/csswizardry/css-for-software-engineers-for-css-developers">Slides</a> | <a href="https://vimeo.com/album/3953264/video/166790749">Video</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-slides-video-2" aria-hidden="true">#</a></h4>
<p>Harry starts his talk with a look back over his family history, highlighting that we have been use modern programming languages since 1959, the year his parents were born. CSS has only been around since 1996, but we have decades more software engineering experience which we can apply to CSS to make our projects more robust, scalable and predictable. I've noted down the core principles Harry advocates below:</p>
<ul>
<li>Don't Repeat Yourself (DRY)
<ul>
<li>Your source should be DRY but the output can have repetition.</li>
<li>Use a preprocessor to store data in variables</li>
<li>Some repetition is better than a bad abstraction</li>
</ul>
</li>
<li>Single Responsibility Principle
<ul>
<li>Use a class to denote a single responsibility, e.g. <em>class="button button-large button-positive"</em></li>
</ul>
</li>
<li>Separation of Concerns
<ul>
<li>Do not use CSS classes for JavaScript and vice versa</li>
<li>Do not bind CSS rules on to accessibility concerns, e.g. <em>role="navigation"</em></li>
<li>Do not write CSS with JavaScript</li>
</ul>
</li>
<li>Immutability
<ul>
<li>Do not allow any CSS rule to change, e.g. with a media query</li>
<li>Use responsive suffixes, as Harry defines in <a href="http://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/#responsive-suffixes">BEMIT</a></li>
</ul>
</li>
</ul>
<p>I will never look at CSS the same. While a lot of this seems obvious when written down, how many times have you broken these simple rules?!</p>
<hr />
<h3 id="sara-soueidan-svg-in-motion" tabindex="-1"><a href="http://twitter.com/@SaraSoueidan">Sara Soueidan</a> - SVG in motion <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#sara-soueidan-svg-in-motion" aria-hidden="true">#</a></h3>
<h4 id="description-slides-pdf-video" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#svg-in-motion">Description</a> | <a href="https://sarasoueidan.com/slides/SVG-In-Motion.pdf">Slides (PDF)</a> | <a href="https://vimeo.com/album/3953264/video/166790778">Video</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-slides-pdf-video" aria-hidden="true">#</a></h4>
<p>Sara is well-known as the SVG guru, in this talk she guides us through the perils and pitfalls of embedding SVG in our applications. It turns out that the way you embed an SVG image directly impacts what you can do with it. The table below summarises how you can animate SVG based on the way it is embedded, with the most flexible being an <svg> element. This has the drawback of increasing your HTML size and reducing cache-ability, however.</p>
<table>
<thead>
<tr>
<th style="text-align:center">Embedding Technique</th>
<th style="text-align:center">CSS Animations</th>
<th style="text-align:center">JS Animations</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center"><img></td>
<td style="text-align:center">Inside <svg></td>
<td style="text-align:center">N/A</td>
</tr>
<tr>
<td style="text-align:center">url();</td>
<td style="text-align:center">Inside <svg></td>
<td style="text-align:center">N/A</td>
</tr>
<tr>
<td style="text-align:center"><picture></td>
<td style="text-align:center">Inside <svg></td>
<td style="text-align:center">N/A</td>
</tr>
<tr>
<td style="text-align:center"><iframe></td>
<td style="text-align:center">Inside <svg></td>
<td style="text-align:center">Anywhere</td>
</tr>
<tr>
<td style="text-align:center"><object></td>
<td style="text-align:center">Inside <svg></td>
<td style="text-align:center">Anywhere</td>
</tr>
<tr>
<td style="text-align:center"><svg></td>
<td style="text-align:center">Anywhere</td>
<td style="text-align:center">Anywhere</td>
</tr>
</tbody>
</table>
<p>Sara then details the various methods of animating SVG, with the following table summarising her recommendations:</p>
<table>
<thead>
<tr>
<th>Animation</th>
<th style="text-align:right">Technique</th>
</tr>
</thead>
<tbody>
<tr>
<td>Transforms</td>
<td style="text-align:right">JavaScript (or CSS)</td>
</tr>
<tr>
<td>Path Morphing</td>
<td style="text-align:right">JavaScript</td>
</tr>
<tr>
<td>Line Drawing</td>
<td style="text-align:right">JavaScript (or CSS)</td>
</tr>
<tr>
<td>Colour & Simple Animations</td>
<td style="text-align:right">CSS (or JavaScript)</td>
</tr>
</tbody>
</table>
<p>The rest of Sara's talk goes in to detail on specific animations, path drawing and some cross-browser issues with transform-origin. Not to mention the fact that CSS transforms do not work in either Internet Explorer nor Microsoft Edge.</p>
<p>The part of Sara's talk I found most interesting was around manipulating the SVG viewbox property. By changing the value of the viewBox, you change the area of the canvas that is visible inside the SVG viewport. This enables you to zoom in to specific areas or objects. This can also enable you to manage SVG sprites and even step-animation in SVG.</p>
<hr />
<h3 id="robin-christopherson-technology-the-power-and-the-promise" tabindex="-1"><a href="http://twitter.com/@USA2DAY">Robin Christopherson</a> - Technology – the power and the promise <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#robin-christopherson-technology-the-power-and-the-promise" aria-hidden="true">#</a></h3>
<h4 id="description" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#technology-the-power-and-the-promise">Description</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description" aria-hidden="true">#</a></h4>
<p>Robin used a selection of videos and personal stories to remind us of how transformative the web is for people with disabilities. Even with the increased focus that developers put on accessibility, many web sites are totally inaccessible to blind people like Robin. New technologies such as automated intelligence, voice recognition and driver-less cars are all going to help people with disabilities, if only the designers and developers bear that in mind.</p>
<hr />
<h3 id="frederik-vanhoutte-rendering-the-obvious" tabindex="-1"><a href="http://twitter.com/@wblut">Frederik Vanhoutte</a> - Rendering the Obvious <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#frederik-vanhoutte-rendering-the-obvious" aria-hidden="true">#</a></h3>
<h4 id="description-1" tabindex="-1"><a href="http://2016.render-conf.com/talks.php#rendering-the-obvious">Description</a> <a class="direct-link" href="https://simonhearne.com/2016/render-conf-1/#description-1" aria-hidden="true">#</a></h4>
<p>Frederik started his talk with a reference to web performance - render the obvious. He mentions that we should show customers what they've got as soon as they've got it. From there the talk rapidly morphed into a psychedelic exploration of teaching and knowledge, using 'how a rainbow works' as a metaphor of a knowledge abstraction.</p>
<p>Children don't need to know exactly how a rainbow works, so a simple demonstration of a prism refracting light and creating the colours of the rainbow suffices. This is an abstraction away from how rainbows actually work, involving reflection and refraction in spheres of water. You can't hope to know everything, so accepting abstractions of knowledge in some areas is fine, but you must understand that everything you know is an abstraction...</p>
Prioritising Site Speed Recommendations2016-01-18T00:00:00Zhttps://simonhearne.com/2016/prioritising-sitespeed-recommendations/<p>One of my favourite tasks as a consultant is to provide reports to clients which make recommendations for improving site speed. Often these reports are taken very seriously, with senior managers using them to help drive the development roadmap for the next few months.</p>
<p>The reason I enjoy these reports so much is that you can often see a direct positive change off the back of them. Like most IT professionals I miss the intrinsic pleasure of working hard to create something with my hands, to have the <a href="http://changingminds.org/explanations/needs/completion.htm">sense of completion</a> at the end of a project.</p>
<p>Seeing changes based on my recommendations, speed indexes improving and conversion rate increasing, is one of the few 'tactile' pieces of feedback I have on my work.</p>
<blockquote>
<p>The vast majority of the improvements are best practises and relatively easy to observe</p>
</blockquote>
<p>Perhaps the most surprising part of delivering these reports is that quite a few of the recommendations I make are already in the client's backlog. Luckily there is invariably at least one 'aha' moment when something truly unknown is discovered, but the vast majority of the improvements are best practises and relatively easy to observe.</p>
<p>The last few of these types of engagements have led me to a realisation; for most of my clients these reports are used as a formal document to <em>prioritise</em> site speed improvement work, not just to discover it.</p>
<blockquote>
<p>There's little value in telling a client about the best practices they've already implemented.</p>
</blockquote>
<p>My document structure has evolved due to this. There's little value in telling a client about the best practices they've already implemented, nor is there any point in documenting the fact that they could shave another 10kB off their image weight.</p>
<p>So in my reports I now suggest that site speed optimisations can be ranked by two critical factors:</p>
<ul>
<li>what impact will this change have on the users</li>
<li>how hard will the change be to implement</li>
</ul>
<p>If you can put a rough estimate for these two factors against each recommendation then you can rapidly determine the highest priorities for development. The <a href="https://en.m.wikipedia.org/wiki/MoSCoW_method">MoSCoW</a> Method gives a good methodology around describing priorities and I often reduce this to a three star system: ★ is could do up to ★★★ for must do. For the ease of implementation ranking: ★ means difficult and ★★★ is simple to change.</p>
<p>I've borrowed from a recent report to demonstrate this theory in practise, below is a small section taken from the results table:</p>
<table>
<thead>
<tr>
<th style="text-align:right">Recommendation</th>
<th style="text-align:center">Impact on Users</th>
<th style="text-align:center">Ease of Implementation</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:right">Enable gzip on CSS</td>
<td style="text-align:center">★★★</td>
<td style="text-align:center">★★★</td>
</tr>
<tr>
<td style="text-align:right">Reduce upstream cookie size</td>
<td style="text-align:center">☆★★</td>
<td style="text-align:center">☆★★</td>
</tr>
<tr>
<td style="text-align:right">Replace icon fonts with SVG</td>
<td style="text-align:center">☆★★</td>
<td style="text-align:center">☆☆★</td>
</tr>
<tr>
<td style="text-align:right">Review third-party content</td>
<td style="text-align:center">☆☆★</td>
<td style="text-align:center">☆☆★</td>
</tr>
<tr>
<td style="text-align:right">Remove redundant JavaScript</td>
<td style="text-align:center">☆★★</td>
<td style="text-align:center">☆☆★</td>
</tr>
<tr>
<td style="text-align:right">Remove redirects</td>
<td style="text-align:center">☆☆★</td>
<td style="text-align:center">☆★★</td>
</tr>
</tbody>
</table>
<p>The sum of the stars give a good idea of priority. Take the uncompressed CSS issue for example: enabling gzip compression should be trivial on any modern set-up, and as CSS is critical to render a page it should be delivered as soon as possible.</p>
<blockquote>
<p>I place the highest priority issues in the executive summary, with the full table in the conclusion.</p>
</blockquote>
<p>This gives enough up front to give an idea of the issues that have been found, with the addition of the full list for internal prioritisation.
The rest of the report describes the methodology for making the recommendations, as well as how each MoSCoW recommendation is made.</p>
<p>For the structure of the recommendations, I usually break it down into four of these main focus areas:</p>
<ul>
<li>Reduce Requests</li>
<li>Reduce Transmitted Bytes</li>
<li>Improve Render Performance</li>
<li>Review Third-Party Content</li>
<li>Improve Back-end Performance</li>
</ul>
<p>In each one I detail what has been discovered, why it is important and how it can be fixed. Perhaps there's another post in that.</p>
Velocity Europe 2015 - The Good Ones!2015-11-01T19:54:00Zhttps://simonhearne.com/2015/velocity-europe-2015-report-2/<blockquote>
<p><a href="https://simonhearne.com/2015/velocity-europe-2015-report-1/">Another post</a> lists the talks that got five stars from me!</p>
</blockquote>
<h3 id="continuous-performance-stijn-polfliet-coscale" tabindex="-1">Continuous Performance - <em>Stijn Polfliet (CoScale)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#continuous-performance-stijn-polfliet-coscale" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/45495">Description</a> | <a href="https://www.youtube.com/watch?v=D4tVpzj6BvE">Video (5m)</a> | <a href="http://cdn.oreillystatic.com/en/assets/1/event/134/Continuous%20performance%20Presentation.pptx">Slides (ppt)</a> | My Rating: ★★★★☆ | Average Rating: ★★★★☆</strong></p>
<p>Stijn's keynote piece was only short (as all sponsored keynotes should be) but he used the time well to highlight CoScale's killer feature.</p>
<p>It appears that CoScale may have solved the biggest problem we have in Web Performance - integrating site speed and business performance data. Everyone in our industry knows that site speed is key to business performance, but we often struggle to spread that message beyond the IT department. By integrating business metrics and performance data we can make better technical decisions and tell a better story about the impact site speed has on our business.</p>
<p>It's worth stating here that <a href="https://www.google.co.uk/analytics/">Google Analytics</a>, <a href="http://www.bluetriangletech.com/">Blue Triangle</a> and <a href="http://www.soasta.com/performance-monitoring/">SOASTA's mPulse</a> also collect and report on this data. It remains to be seen who will win over this currently narrow segment of the market.</p>
<p>Stijn lost a star from me as the talk was not as engaging as it could have been and felt like a sales pitch.</p>
<hr />
<h3 id="stranger-danger-guy-podjarny-and-assaf-hefetz-snyk" tabindex="-1">Stranger danger - <em>Guy Podjarny & Assaf Hefetz (Snyk)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#stranger-danger-guy-podjarny-and-assaf-hefetz-snyk" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/46147">Description</a> | <a href="http://www.youtube.com/watch?v=iXA14OFXxZA">Video (14m)</a> | My Rating: ★★★★☆ | Average Rating: ★★★★☆</strong></p>
<p>Guy and Assaf got 15 minutes to demonstrate <a href="https://snyk.io/">Snyk.io</a> in their (sponsored?) keynote.</p>
<p><a href="http://snyk.io/">Snyk.io</a> (So Now You Know) solves a problem that I didn't know we had - but that I now realise is a huge issue. This problem is that we all use third-party code when we develop applications, and through both direct and indirect dependencies we are continually introducing a huge threat landscape to our applications. Who knows how often vulnerabilities are introduced into open source code, which then get pushed into other code through dependency updates? <a href="http://snyk.io/">Snyk.io</a> do.</p>
<blockquote>
<p>The typical Node.js app has over 200 dependencies and 59% of reported vulnerabilities in Maven packages remain unfixed. The mean-time-to-repair (MTTR) for a Maven package vulnerability is <em>390 days</em>. This presents a serious threat to projects using these dependencies and at present it is very hard to get any visibility of that threat.</p>
</blockquote>
<p><a href="http://snyk.io/">Snyk.io</a> is run from the command line (which calls their remote API service). It can analyse all dependencies in a project and automatically identify <em>known</em> vulnerabilities. The killer feature, though, is the ability to automatically patch the vulnerabilities and update your configuration to maintain the patched version in future updates. Very cool.</p>
<p><a href="http://snyk.io/">Snyk.io</a> currently only works with Node.js projects but they will be expanding to other languages shortly. This is definitely a security service to watch.</p>
<p>Guy and Assam lost a star from me as the demo could have been done in less time and the talk felt a little like a hard sell on <a href="http://snyk.io/">Snyk.io</a>, even though it sells itself!</p>
<hr />
<h3 id="a-paas-for-government-anna-shipman-government-digital-service" tabindex="-1">A PaaS for government - <em>Anna Shipman (Government Digital Service)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#a-paas-for-government-anna-shipman-government-digital-service" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/46814">Description</a> | <a href="https://gdstechnology.blog.gov.uk/2015/10/27/looking-at-open-source-paas-technologies/">Blog Post</a> | <a href="https://www.youtube.com/watch?v=OLOaq-Xf5zU">Video (15m)</a> | My Rating: ★★★☆☆ | Average Rating: ★★★★☆</strong></p>
<p>Anna took us through the thought process of the Government Digital Service as they introduce a Platform as a Service (PaaS) for government websites.</p>
<p>Anna did a great job in introducing what GDS do and how important it is that people can quickly access their services. A few examples were given such as the ability to book a prison visitation and voter registration.</p>
<p>The government has a problem, though. For each new digital service that goes live (over 800 of them are on the cards!) the project team procures the infrastructure, development environment, database, logging, monitoring etc. This makes for a lot of repetition and a slow time-to-market for new services.</p>
<p>Anna's team are hoping to make the release of a new digital service quick and simple by introducing a Government-wide PaaS. She takes us through the thought process of starting a PaaS at government scale - the challenges that are faced when choosing the correct platform as well as service provider. We were shown a number of candidates and a run-down of pro's and con's to each of them and were promised updates on the <a href="https://gdstechnology.blog.gov.uk/">GDS Blog</a> as the PaaS is rolled out. I'll certainly be following that closely.</p>
<p>Anna lost a couple of stars from me as the talk was dry and could have focussed more on the human element - who would be helped by this and how much would it affect them? What are the barriers to adoption within the Government?</p>
<hr />
<h3 id="probabilistically-sampling-a-stream-of-events-with-a-sketch-baron-schwartz-vividcortex" tabindex="-1">Probabilistically sampling a stream of events with a sketch - <em>Baron Schwartz (VividCortex)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#probabilistically-sampling-a-stream-of-events-with-a-sketch-baron-schwartz-vividcortex" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/43888">Description</a> | <a href="http://cdn.oreillystatic.com/en/assets/1/event/134/Probabilistically%20sampling%20a%20stream%20of%20events%20with%20a%20sketch%20Presentation%201.pdf">Slides (pdf)</a> | My Rating: ★★★☆☆ | Average Rating: ★★★★☆</strong></p>
<p>In this talk Baron took us on a technical tour of the event sampling algorithm behind <a href="https://www.vividcortex.com/">VividCortex</a>. VividCortex is a cloud based intelligence platform for database system performance and health, it works with all of the big open source database players.</p>
<p>The challenge that Baron described is that the agents installed on customer hardware generate huge volumes of data; sending all of this data back to VividCortex would be infeasible due to both network constraints and the potential resource impact on the customers' host servers.</p>
<p>To overcome this, VividCortex takes a sampling approach to events. While time-series data is sampled once a second, full event traces are sampled on a less frequent basis (about one per hour per category). The challenge here comes in choosing which events to sample. Baron was open about their choices failing a few times and described a number of approaches that were attempted before detailing their chosen methodology.</p>
<p>I felt that the talk became too technical near the end in detailing exactly how the statistical sketch was created, with not enough high-level description of how it can be applied and, more importantly for the audience, where else this kind of sampling could be used. Less technicality in the talk but with a reference to a technical blog post to back it up may have worked better.</p>
<p>Baron lost a couple of stars from me for this reason, as well as the strong feeling of being sold to. It is unfortunate as the general concept is very interesting to me and I would have loved for this to be a five-star talk.</p>
<hr />
<h3 id="fake-it-until-you-make-it-jean-pierre-vincent-braincracking" tabindex="-1">Fake it until you make it - <em>Jean-Pierre Vincent (BrainCracking)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#fake-it-until-you-make-it-jean-pierre-vincent-braincracking" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44543">Description</a> | <a href="https://docs.google.com/uc?id=0ByrWN83fytRxQzc3b01wQmJOaEE&export=download">Slides (ppt)</a> | My Rating: ★★★★☆ | Average Rating: ★★★★☆</strong></p>
<p>In this talk JP uses stories from his clients to show how interface design can overcome some of our inherent performance challenges.</p>
<p>JP's first example was of work he did on a container shipping company. Their challenge was that the web application to book and manage multi-million dollar shipments took sixty seconds to load for Chinese customers. Understandably these customers raised complaints about performance. After doing some work to halve the page load time complaints were still coming in. After all, it was still a thirty second page load!</p>
<p>As a developer with an eye for design, JP found a design solution - he added a spinner to indicate loading on the application page. Complaints dropped a little. He then found another design solution - a five second animation of a seagull flying across the screen. This significantly reduced complaints.</p>
<p>A number of other examples were given of how we, as web developers and designers, should consider performance by design. This is something that <a href="http://larahogan.me/design/">Lara Hogan</a> has been preaching for quite some time. A new idea to me was using the well-known research showing that we must give feedback within 200ms and show progress within 1000ms. JP showed us how mobile applications in particular are following these rules well. To keep the user engaged they use transition animations between actions, hiding the delay in downloading or preparing content while giving the user a feeling of continuity.</p>
<p>I think JP's conclusions could have been stronger - for example giving your user feedback within 200ms and designing transition pieces to keep the user involved. Overall, though, the talk was very strong and introduced or reinforced a large number of key performance concepts for me.</p>
<hr />
<h3 id="bigger-faster-and-more-engaging-while-on-a-budget-nathan-bower-zillow" tabindex="-1">Bigger, faster, and more engaging while on a budget - <em>Nathan Bower (Zillow)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#bigger-faster-and-more-engaging-while-on-a-budget-nathan-bower-zillow" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/46156">Description</a> | My Rating: ★★★★☆ | Average Rating: ★★★☆☆</strong></p>
<p>Nathan introduced <a href="http://www.zillow.com/">Zillow</a> as one of the biggest property websites in the United States. Zillow have faced a number of challenges around web performance, Nathan detailed one challenge in particular as a case study - when the mobile homepage size increased by 12MB.</p>
<p>By using <a href="https://speedcurve.com/">SpeedCurve</a> and performance budgets Nathan was alerted to this immediately, he even managed to get web developers on the case while he was commuting to work.</p>
<p>It turned out that an A/B testing feature that was not being used was pulling down a large amount of content which was never even rendered.</p>
<p>Nathan's conclusions were that performance budgets are critical, but they must be on the right metrics. His main suggestion was to use the <a href="http://www.stevesouders.com/blog/2014/08/21/resource-timing-practical-tips/">User Timing API</a> to set custom metrics and track those as well as <a href="https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index">SpeedIndex</a> and the more traditional metrics. He also concluded that you can download a whole load of useless data on mobile devices and customers don't care - I'm not sure that I would repeat that one too much!</p>
<hr />
<h3 id="finding-bad-apples-early-minimizing-performance-impact-arun-kejariwal-machine-zone" tabindex="-1">Finding bad apples early: Minimizing performance impact - <em>Arun Kejariwal (Machine Zone)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#finding-bad-apples-early-minimizing-performance-impact-arun-kejariwal-machine-zone" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/43716">Description</a> | My Rating: ★★★☆☆ | Average Rating: ★★★☆☆</strong></p>
<p>Arun used to work at Twitter where he helped to develop an open source <a href="https://github.com/twitter/AnomalyDetection">anomaly detection library</a>. In this talk he presented how he uses anomaly detection to find bad server nodes in large clusters at his new job with <a href="https://www.machinezone.com/">Machine Zone</a> (they make the hugely popular mobile game <a href="http://www.gameofwarapp.com/">Game Of War</a>).</p>
<p>I was unfortunately a little lost by the technicality of the presentation and would have loved more visuals to explain what Arun was talking about - he is obviously an extremely clever chap.</p>
<p>The general idea, I believe, was that he uses clustering on time-series data such as CPU and RAM utilisation across all nodes to identify node behaviour. Once these known-good patterns are learned, anomalies can be detected by comparing the timeseries data of each node against all node clusters. This is performed using the <a href="https://en.wikipedia.org/wiki/Euclidean_distance">Euclidean distance measure</a> between the data matrices of each cluster-median against the individual node.</p>
<p>The outcome from this work is that the system can now automatically identify nodes which are behaving abnormally in the context of their peers. This allowed Arun to successfully identify a heterogenous server environment where two different types of CPU architecture had been mistakenly installed in nodes in the same cluster.</p>
<p>Arun lost a couple of stars from me as I found it hard to follow the presentation and there were too few conclusions presented. What else can this type of anomaly detection be used for? And will it ever be open source? (The tool discussed in this presentation is different to the open source library mentioned at the beginning).</p>
<hr />
<h3 id="designing-for-security-outcomes-eleanor-saitta-dymaxion-org" tabindex="-1">Designing for Security Outcomes - <em>Eleanor Saitta (<a href="http://dymaxion.org/">Dymaxion.org</a>)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#designing-for-security-outcomes-eleanor-saitta-dymaxion-org" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/46786">Description</a> | <a href="https://www.youtube.com/watch?v=zqnQ0Mvi-as">Video (18m)</a> | <a href="http://cdn.oreillystatic.com/en/assets/1/event/134/Designing%20for%20security%20outcomes%20Presentation.pdf">Slides (pdf)</a> | My Rating: ★★★☆☆ | Average Rating: ★★★☆☆</strong></p>
<p>Eleanor introduced her keynote by telling us we've got security all wrong, that we focus on technology and not people. She then tells us how we can get better at designing our teams and applications with security in mind.</p>
<blockquote>
<p>Security design is the process of
understanding user culture, goals,
and workflows, organizational
technical capabilities, and adversary
capabilities and dispositions and
synthesizing a satisficing solution.</p>
</blockquote>
<p>Eleanor suggests that we should all be building thorough threat models (rather than threat lists) at the very early stages of projects. This will allow us to capture potential security concerns early, as we will have a better understanding of the potential adversaries we face.</p>
<p>I'm afraid I got lost at a few points in Eleanor's keynote, the slides were occasionally distracting and some un-introduced security terms threw me off. That's why she lost a couple of stars from me.</p>
<hr />
<h3 id="a-day-in-the-life-an-immersive-data-experience-david-boloker-ibm-corporation" tabindex="-1">A day in the life - An immersive data experience - <em>David Boloker (IBM Corporation)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#a-day-in-the-life-an-immersive-data-experience-david-boloker-ibm-corporation" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/47746">Description</a> | <a href="https://www.youtube.com/watch?v=6vMR6p-ff9U">Video (10m)</a> | My Rating: ★★★☆☆ | Average Rating: ★★★★☆</strong></p>
<p>David had a hard job in his sponsored keynote. He started by introducing the limitations we currently have with our Human-Computer Interaction (HCI) and that we need a more natural way to communicate with machines.</p>
<p>To demonstrate this David quickly switched into a live demonstration of a piece of conversational software. The demo was unfortunately contrived and awkward; the speech recognition did not work well in the keynote setting, the feedback from the software was slow and machine-like and the quality of data was not much better than you would get from Siri.</p>
<hr />
<h3 id="thinking-about-thinking-about-people-courtney-w-nash-o-reilly" tabindex="-1">Thinking about thinking (about people) - <em>Courtney W. Nash (O'Reilly)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#thinking-about-thinking-about-people-courtney-w-nash-o-reilly" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/48025">Description</a> | <a href="https://www.youtube.com/watch?v=tkJtr6xJp4g">Video (10m)</a> | My Rating: ★★★★☆ | Average Rating: ★★★★☆</strong></p>
<p>Courtney's background is in cognitive science and that really comes across in this keynote.</p>
<p>I think Courtney's general point is that humans are not inherently sensible or unbiased. We are weird creatures that do not follow the rules. An example was given of a 'shared space' created in a town which mixed pedestrians, cyclists and drivers with no traffic lights or stop signs. This forced people to be more considerate of those around them and in-turn increased safety.</p>
<p>This 'shared space' was eventually reverted, though. Even though empirical safety had increased significantly, the residents still <em>felt</em> that the junction was less safe and campaigned against it.</p>
<p>Courtney then related this to the web performance industry by citing an <a href="https://www.uie.com/articles/download_time/">article</a> published in User Interface Engineering in 2001. The article states that if a user achieves what they set out to do on a website, they perceive it as 'fast'. This perception of speed, however, has no correlation to the actual speed of the site! Courtney's conclusion from this is that we need to measure and monitor better metrics, <a href="http://blog.alexmaccaw.com/time-to-first-tweet">time to first tweet</a> was referenced.</p>
<p>Courtney then took a turn towards operations for a conclusion that everyone should share responsibility for the uptime of a site or application, where uptime is measured as the ability of a customer to complete their action.</p>
<hr />
<h3 id="measuring-the-performance-of-single-page-web-applications-philip-tellis-and-nic-jansma-soasta" tabindex="-1">Measuring the performance of single page web applications - <em>Philip Tellis & Nic Jansma (SOASTA)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#measuring-the-performance-of-single-page-web-applications-philip-tellis-and-nic-jansma-soasta" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44019">Description</a> | My Rating: ★★★★☆ | Average Rating: ★★★★☆</strong></p>
<p>Philip and Nic work on SOASTA's Real User Measurement (RUM) product mPulse. In this talk they gave a run-down on the work they are doing with <a href="https://github.com/lognormal/boomerang/">boomerang</a> to improve the data captured on single-page applications (SPAs).</p>
<p>SPAs give us a fundamental challenge when it comes to monitoring - the old paradigm of clicking a link causing a page navigation which can be tracked is no more. When SOASTA enabled SPA measurement for one client the amount of datapoints collected increased four-fold!</p>
<p>The work Philip and Nic are doing is around developing <a href="https://github.com/lognormal/boomerang/tree/soasta-2015/plugins">plugins</a> for boomerang which instrument the network activity created by SPAs.</p>
<p>The conclusion is that browsers need to catch up with vendors - it should not be so difficult to monitor and measure real users.</p>
<hr />
<h3 id="open-components-as-microservices-in-the-front-end-world-matteo-figus-opentable" tabindex="-1">Open components as microservices in the front-end world - <em>Matteo Figus (OpenTable)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#open-components-as-microservices-in-the-front-end-world-matteo-figus-opentable" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44289">Description</a> | My Rating: ★★★★☆ | Average Rating: ★★★☆☆</strong></p>
<p>In this talk Matteo introduced his open source project <a href="https://github.com/opentable/oc">open components</a>. Matteo describes how difficult it is to develop a website in the complex environment at OpenTable, with many engineers based out of multiple countries all working on the same site.</p>
<p>To solve the challenges faced with front-end development at OpenTable, they decided to break the development down to micro-sites. By utilising common components and removing interdependencies the developers can now rapidly develop these micro-sites and push to live indepently of each other.</p>
<p>Open components provides a registry and commandline service to create front-end components.</p>
<hr />
<h3 id="a-real-life-account-of-moving-100-to-a-public-cloud-julien-simon-aws-and-antoine-guy-viadeo" tabindex="-1">A real-life account of moving 100% to a public cloud - <em>Julien Simon (AWS) & Antoine Guy (Viadeo)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-2/#a-real-life-account-of-moving-100-to-a-public-cloud-julien-simon-aws-and-antoine-guy-viadeo" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44042">Description</a> | My Rating: ★★★☆☆ | Average Rating: ★★★★☆</strong></p>
<p>Julien and Antoine were part of the team that migrated <a href="http://gb.viadeo.com/en/">Viadeo</a> to a public cloud stack. In this talk they describe the technical process they went through to get the site successfully migrated.</p>
<p>The main outcome of the migration was the move to <a href="https://aws.amazon.com/cloudformation/">CloudFormation</a> to automate Infrastructure as Code.</p>
Velocity Europe 2015 - My Allstars2015-11-01T19:54:00Zhttps://simonhearne.com/2015/velocity-europe-2015-report-1/<blockquote>
<p><a href="https://simonhearne.com/2015/velocity-europe-2015-report-2/">Another post</a> lists the talks that didn't quite get five stars from me!</p>
</blockquote>
<h3 id="the-physical-web-is-a-speed-issue-scott-jenson-google" tabindex="-1">The Physical Web is a Speed Issue - <em>Scott Jenson (Google)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-1/#the-physical-web-is-a-speed-issue-scott-jenson-google" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/45231">Description</a> | <a href="https://www.youtube.com/watch?v=7H_E_ZbFAn0">Video (18m)</a> | My Rating: ★★★★★ | Average Rating: ★★★★☆</strong></p>
<p>Scott's keynote was probably the highlight of this Velocity for me. He discusses how the web will change over the next few years with the advent of the Physical Web and the Internet Of Things.</p>
<p>Scott used a number of examples to show how this will affect our daily lives, with a parking meter being the clearest demonstration.</p>
<p>Imagine a parking meter which has a light-weight web server and low-energy bluetooth. The bluetooth (or other low-energy, low-range wireless technology) continuously publishes a unique URL for the web server. You park your car and pull out your phone, in your notification tray this URL has silently appeared. You click on the link and immediately load a payment page for the parking meter, one tap and you've got an hour's worth of parking and the meter screen immediately updates.</p>
<p>This sounds pretty futuristic, but almost all of the technology we need is already in place. The one part that's missing is the silent notifications - broadcasting a link that nearby devices can pick up but without irritating every passer-by (like iBeacon). Scott's team at Google is working on this very challenge, there is an open source repository with a functioning Chrome extensios on iPhone (but not Android!) on <a href="https://github.com/google/physical-web">GitHub</a>. This technology is based on Google's open Bluetooth low-energy protocol <a href="https://github.com/google/eddystone">Eddystone</a>.</p>
<p>The title of Scott's talk took me a while to decipher: "The Physical Web is a Speed Issue". By this I think he means that the Physical Web will make our day-to-day lives more 'speedy'. This will be achieved by automating link sharing, thus making tedious tasks such as paying for parking or interacting with every-day items much easier and faster. In order for this to work, though, we need <em>fast mobile web applications</em>.</p>
<p>We cannot and will not install an application for every light bulb, parking meter or door lock which makes up the Physical Web. Technologies such as <a href="https://github.com/slightlyoff/ServiceWorker/">service worker</a> and <a href="https://www.chromium.org/quic">quic</a> are going to be essential to make the Physical Web fast and, more important, accessible.</p>
<blockquote>
<p>The Physical Web is like a really awesome QR Code</p>
</blockquote>
<p>This talk really hit home for me. As someone who runs two QR Code services (<a href="https://qrexplore.com/">QR Explore</a> and <a href="http://thisqr.com/">ThisQR</a>) I am more than familiar with the drawbacks of QR Codes in western society - no one wants to be seen taking a picture of a poster! This means that QR Codes work best in a closed environment such as a factory (which is why they were invented) or a science center. The potential for having QR Code-like functionality (link discovery) without the awkward scanning issue is an absolute game-changer.</p>
<hr />
<h3 id="discovering-operations-expertise-john-allspaw-etsy" tabindex="-1">Discovering Operations Expertise - <em>John Allspaw (Etsy)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-1/#discovering-operations-expertise-john-allspaw-etsy" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/47779">Description</a> | <a href="https://www.youtube.com/watch?v=OJ21jwJThq4">Video (20m)</a> | <a href="http://bit.ly/AllspawThesis">Thesis (pdf)</a> | My Rating: ★★★★★ | Average Rating: ★★★★☆</strong></p>
<p>John's keynote was a very brief summary of his work towards a thesis in Human Factors Engineering.</p>
<p>We often focus on analysing failures to find out what has gone wrong. John's point was that we need to focus on success as well in order to find out why something has not gone wrong. No matter how much we automate, humans will always be the lynchpin of success or failure in deployment; if we analyse what comes together to make successful human behaviour then we can improve success rate at the individual level.</p>
<p>John suggests that in order to increase efficiency, especially in times of crisis, we also need to find out what engineers do in the event of failure - what are the rules of thumb that are followed? Once we know that, we can analyse and improve our processes.</p>
<p>This talk was a refreshing change from the technical focus of the conference, instead proposing that as humans are the critical factors in all that we do, so we must focus there first.</p>
<hr />
<h3 id="ensuring-a-high-performing-web-for-the-next-billion-people-bruce-lawson-opera-asa" tabindex="-1">Ensuring a high performing web for the next billion people - <em>Bruce Lawson (Opera ASA)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-1/#ensuring-a-high-performing-web-for-the-next-billion-people-bruce-lawson-opera-asa" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44920">Description</a> | <a href="https://www.youtube.com/watch?v=f6As5HEkG5E">Video (20m)</a> | My Rating: ★★★★★ | Average Rating: ★★★★★</strong></p>
<p>The highest rated talk of Velocity. I saw Bruce give this talk at Velocity Santa Clara earlier this year and it has got even better.</p>
<p>Few of us consider proxy browsers when we develop websites, but Opera Mini alone serves over 350 Million customers. By developing Javascript-rich websites or by using icon fonts we may be excluding a huge portion of our future market.</p>
<hr />
<h3 id="one-year-later-how-marks-and-spencer-revolutionized-perfops-after-velocity-2014-andrew-neilson-m-and-s" tabindex="-1">One year later: How Marks & Spencer revolutionized PerfOps after Velocity 2014 - <em>Andrew Neilson (M&S)</em> <a class="direct-link" href="https://simonhearne.com/2015/velocity-europe-2015-report-1/#one-year-later-how-marks-and-spencer-revolutionized-perfops-after-velocity-2014-andrew-neilson-m-and-s" aria-hidden="true">#</a></h3>
<p><strong><a href="http://velocityconf.com/devops-web-performance-eu-2015/public/schedule/detail/44100">Description</a> | My Rating: ★★★★★ | Average Rating: ★★★★☆</strong></p>
<p>Andy runs PerfOps and <a href="http://marksandspencer.com/">marksandspencer.com</a> - a large UK retailer that does 20% of its business online. In this talk he described how Velocity Europe 2014 inspired him to refactor performance operations at M&S.</p>
<p>Andy gave a practical run-down of the tools and metrics that M&S use to track performance with a number of examples to illustrate his points.</p>
<p>A key turning point for the business was when SpeedCurve showed a huge increase in third-party calls on the M&S homepage. This led Andy to investigate what had happened using my <a href="http://requestmap.webperf.tools/">requestmap</a> and discovered that a third-party service had started calling more third-party domains. This discovery led to a piece of analysis showing that third-parties were responsible for 50% of the load time on the homepage.</p>
Measuring Webpage Jank2015-09-11T12:00:00Zhttps://simonhearne.com/2015/jank-meter/<p>Here's the bookmarklet:
<a style="cursor:alias;" class="btn btn-block" href="javascript:(function(e){function t(){for(var e=document.getElementsByClassName("fpser-lay"),t=e.length-1,n=t;n>=0;n--)try{document.body.removeChild(e[n])}catch(r){continue}var r=document.getElementById("fpser");r&&r.parentNode.removeChild(r)}function n(){if(t(),e.scrollTo(0,0),count=0,sh=Math.max(document.documentElement.clientHeight,e.innerHeight||0),h=Math.max(document.body.offsetHeight,document.body.scrollHeight)-sh,chunk=100,h-100<0)throw alert("I need to scroll, please reduce your browser height or choose a longer page!"),"Too short";fA=[],e.requestAnimationFrame(r),ll=!1}function r(){return ll?(e.scrollBy(0,chunk),tl=performance.now(),fA.push(parseInt(1e3/(tl-ll))),ll=tl,count+=chunk,void(count%3E=h?(e.scrollTo(0,0),l(fA)):e.requestAnimationFrame(r))):(ll=performance.now(),void%20e.requestAnimationFrame(r))}function%20o(e){d=a(e,100);var%20t=350,n=1,r=Math.max(parseInt(t/e.length-n),2),o=%22https://chart.googleapis.com/chart?chbh=%22+r+%22,%22+n+%22&cht=bvs&chxt=y&chf=bg,s,00000000&chs=350x60&chm=D,FF0000,1,0,2,1&chd=%22+d;return%20console.log(o.length),o.substr(0,o.length-1)}function%20a(e,t){var%20n=%22e:%22;for(i=0,len=e.length;i%3Clen;i++){var%20r=new%20Number(e[i]),o=Math.floor(s*s*r/t);if(o%3Es*s-1)n+=%22..%22;else%20if(0%3Eo)n+=%22__%22;else{var%20a=Math.floor(o/s),l=o-s*a;n+=c.charAt(a)+c.charAt(l)}}return%20n}function%20l(e){var%20t=e.slice(0);e.sort(function(e,t){return%20e-t});var%20n=Math.floor(e.length/2);if(e.length%2)var%20r=e[n];else%20var%20r=(e[n-1]+e[n])/2;for(var%20a=e[0],l=e[e.length-1],i=0,c=0;c%3Ce.length;c++)i+=e[c];var%20s=parseInt(i/e.length),p=o(t),d=document.createElement(%22div%22);d.id=%22fpser%22,d.style.cssText+=%22;font-family:sans-serif;font-weight:bold;width:400px;background:rgba(200,200,200,0.9);position:fixed;top:10px;right:10px;z-index:10002;padding:5px;box-shadow:%200px%200px%205px%202px%20rgba(0,0,0,0.75);text-align:center%22;var%20m=document.createElement(%22p%22);m.innerHTML=%22FPS:%20median%20=%20%22+r+%22,%20mean%20=%20%22+s+%22,%20min%20=%20%22+a+%22,%20max%20=%20%22+l,p2=document.createElement(%22p%22),50%3E=r?(p2.style.color=%22red%22,p2.innerHTML=%22This%20page%20is%20JANKY%20(Framerate%20is%20below%2050%20FPS)%22):r%3E=59?(p2.style.color=%22green%22,p2.innerHTML=%22This%20page%20is%20SMOOTH%20(Framerate%20around%2060%20FPS)%22):(p2.style.color=%22yellow%22,p2.innerHTML=%22This%20page%20is%20ALMOST%20JANKY%20(Framerate%20above%2050%20FPS)%22),p3=document.createElement(%22p%22),p3.innerHTML=%22%3Ca%20href=\%22https://simonhearne.com/2015/jank-meter\%22%3EWhat%20is%20this?%3C/a%3E%20|%20%3Ca%20href=\%22#\%22%20onclick=\%22window.FPS.cl();\%22%3EHide%20this%3C/a%3E.%22;var%20c=document.createElement(%22img%22);c.src=p,d.appendChild(m),d.appendChild(c),d.appendChild(p2),d.appendChild(p3),document.body.appendChild(d);for(var%20u=0,c=sh;c%3C=h+sh;c+=100){var%20p=document.createElement(%22div%22);p.className=%22fpser-lay%22;var%20g=%22%22;if(g=parseInt(t[u])%3C50?%22255,55,0%22:parseInt(t[u])%3C59?%22255,255,0%22:parseInt(t[u])%3E0?%2255,255,55%22:%2255,55,55%22,p.style.cssText+=%22;border-bottom:1px%20solid%20black;line-height:%22+chunk+%22px;text-align:center;font-weight:bold;z-index:10001;position:absolute;width:80px;height:%22+chunk+%22px;left:0;top:%22+c+%22px;background:rgba(%22+g+%22,0.5);%22,parseInt(t[u])%3E0){var%20f=document.createElement(%22p%22);f.style.cssText+=%22;margin:0px%22,f.innerHTML=t[u]+%22%20FPS%22,p.appendChild(f)}document.body.appendChild(p),u++}var%20p=document.createElement(%22div%22);p.className=%22fpser-lay%22,p.style.cssText+=%22;z-index:10001;position:absolute;width:80px;height:%22+sh+%22px;left:0;top:0;background:rgba(55,55,55,0.5);%22,document.body.appendChild(p)}var%20c=%22ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.%22,s=c.length;n(),e.FPS={},e.FPS.cl=t})(window);">Jank Meter</a>
Drag it to your bookmarks bar or just add as a bookmark.</p>
<h2 id="what-is-jank" tabindex="-1">What is jank? <a class="direct-link" href="https://simonhearne.com/2015/jank-meter/#what-is-jank" aria-hidden="true">#</a></h2>
<p>Our eyes can't perceive visual changes at around 60 frames per second (FPS). As such, for smooth visual experiences most modern screens have a refresh rate of 60FPS. In order for our webpages to appear smooth to a user while interacting, all frames must render within 16ms (1000ms / 60 FPS).</p>
<h2 id="what-causes-jank" tabindex="-1">What causes jank? <a class="direct-link" href="https://simonhearne.com/2015/jank-meter/#what-causes-jank" aria-hidden="true">#</a></h2>
<p>Anything that is expensive to process and causes the browser to repaint can cause jank. Common causes include large background images that move on scroll (parallax effect) and Javascript functions bound to scroll events. Paul Lewis (<a href="https://twitter.com/aerotwist">@aerotwist</a>) wrote a <a href="http://calendar.perfplanet.com/2013/the-runtime-performance-checklist/">good post</a> on some of the causes of jank back in 2013.</p>
<p>Here's an example of the jank meter on a web design agency's homepage. They use a jQuery plugin to create a parallax effect on large (but barely visible) background images.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/janky_site2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/bWWrQttj0x-600.avif 600w, https://simonhearne.com/img/bWWrQttj0x-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/bWWrQttj0x-600.webp 600w, https://simonhearne.com/img/bWWrQttj0x-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/bWWrQttj0x-600.jpeg 600w, https://simonhearne.com/img/bWWrQttj0x-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Jank Meter showing poor scroll performance" loading="lazy" decoding="async" src="https://simonhearne.com/img/bWWrQttj0x-600.jpeg" width="900" height="336" /></picture></a><figcaption>Jank Meter showing poor scroll performance</figcaption></figure>
<p>Conversely, here's <a href="http://jankfree.org/">jankfree.org</a> with a near-perfect scrolling experience:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/jankfree_site2.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/DJPheNrNRm-600.avif 600w, https://simonhearne.com/img/DJPheNrNRm-900.avif 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/DJPheNrNRm-600.webp 600w, https://simonhearne.com/img/DJPheNrNRm-900.webp 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/jpeg" srcset="https://simonhearne.com/img/DJPheNrNRm-600.jpeg 600w, https://simonhearne.com/img/DJPheNrNRm-900.jpeg 900w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Jank Meter showing good scroll performance" loading="lazy" decoding="async" src="https://simonhearne.com/img/DJPheNrNRm-600.jpeg" width="900" height="249" /></picture></a><figcaption>Jank Meter showing good scroll performance</figcaption></figure>
<h2 id="how-do-you-detect-jank" tabindex="-1">How do you detect jank? <a class="direct-link" href="https://simonhearne.com/2015/jank-meter/#how-do-you-detect-jank" aria-hidden="true">#</a></h2>
<p>You can detect jank using the Timeline tab in Chrome Developer Tools. Simply hit the record button and scroll, then stop the recording and look for slow frames:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/Xk_U_HWt4W-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Xk_U_HWt4W-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/Xk_U_HWt4W-600.jpeg" width="600" height="480" /></picture><figcaption>Dev Tools showing Jank</figcaption></figure>
<p>You can also enable the framerate overlay in Chrome to show the live render performance:</p>
<figure class=""><picture><source type="image/avif" srcset="https://simonhearne.com/img/VOol7pcWuX-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/VOol7pcWuX-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="" loading="lazy" decoding="async" src="https://simonhearne.com/img/VOol7pcWuX-600.jpeg" width="600" height="156" /></picture><figcaption>Dev Tools showing FPS meter</figcaption></figure>
<p>This can all be a bit time consuming though, recently I've found myself spending a lot of time in the timeline view just to detect jank.
The framerate meter also makes it difficult to identify exactly where in the page the jank is coming from.</p>
<p>I figured that there must be an easier way to do this. <a href="http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/">requestAnimationFrame</a> has been around for a few years now, it lets you make timer-based animations more efficient by synchronising reflow and repaint events.
This also gives us a clue as to how fast the browser can synchronise these events: ideally requestAnimationFrame should call your function every 16ms to get that silky-smooth 60FPS you want. If, however, it calls your function after 50ms you know that there has been something hogging the browser's time when it should have been repainting.
I created a small Javascript bookmarklet around this concept, scrolling down a page and measuring the effective framerate.</p>
<p>The bookmarklet produces a small overlay on the top-right of the screen showing the framerates achieved while scrolling the page. It also adds a bar to the left of the page showing what framerate was achieved for that scroll movement.</p>
<h2 id="jank-meter-limitations" tabindex="-1">Jank meter limitations <a class="direct-link" href="https://simonhearne.com/2015/jank-meter/#jank-meter-limitations" aria-hidden="true">#</a></h2>
<ul>
<li>Style is inherited, so it may look funny</li>
<li>It depends on being able to scroll, so the page has to be longer than 2 x viewport height</li>
<li>It relies on performance.now() and requestAnimationFrame() so it won't work in all browsers (latest Firefox and Chrome are fine)</li>
<li>The chart image may not load on secure sites</li>
</ul>
<p>There are probably a few more limitations, give it a go and let me know if you have any issues.
The code is in <a href="https://gist.github.com/simonhearne/ef145e2732f2082771d3">a gist</a> so feel free to fork.</p>
<h2 id="how-to-fix-jank" tabindex="-1">How to fix jank? <a class="direct-link" href="https://simonhearne.com/2015/jank-meter/#how-to-fix-jank" aria-hidden="true">#</a></h2>
<p>Paul Lewis has a great <a href="https://www.youtube.com/watch?v=QU1JAW5LRKU">live demo</a> on youtube of debugging and fixing jank in realtime.</p>
<p>There are a number of resources available, <a href="https://jankfree.org/">jankfree.org</a> is a great place to start.</p>
Be Mindful With Modernizr2015-08-10T22:02:00Zhttps://simonhearne.com/2015/be-mindful-with-modernizr/<p>According to <a href="http://www.modernizr.com/">Modernizr.com</a>, Modernizr is a JavaScript library that detects HTML5 and CSS3 features in the user's browser.
This framework tests for features in the user's browser then adds classes to your page declaring which HTML5 and CSS3 features are (and are not) supported in that browser.</p>
<p>Recently, a colleague was analysing a client's site and noticed that performance on mobile was about one second slower than expected, with a gap in the WebPageTest waterfall with no network traffic.
We did some digging on desktop and could not replicate the one second of missing network time, although there was a clue hidden in the high CPU usage on the browser main thread, visible in the WebPageTest waterfall:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/waterfall_gap.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/gg0lgml_gu-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/gg0lgml_gu-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="High CPU usage and a gap in the waterfall at 6.5-7.5s" loading="lazy" decoding="async" src="https://simonhearne.com/img/gg0lgml_gu-600.jpeg" width="600" height="502" /></picture></a><figcaption>High CPU usage and a gap in the waterfall at 6.5-7.5s</figcaption></figure>
<p>Something on the page was consuming the browser thread and blocking critical content including web fonts - meaning no text was rendered until this process had completed.
I decided to find more sites using Modernizr to see if the issue could be replicated - it could.</p>
<p>Take <a href="http://www.creativebloq.com/">www.CreativeBloq.com</a> as an example. A Javascript bundle including Modernizr is loaded as object #6 on the homepage.
When loading it in a browser you can quickly see the browser thread being consumed by the Modernizr script in the Chrome Developer Tools Timeline:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/modernizr_desktop.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/DwWjtE_u9C-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/DwWjtE_u9C-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Over 150ms spent in modernizr.js - on a Core i7 processor" loading="lazy" decoding="async" src="https://simonhearne.com/img/DwWjtE_u9C-600.jpeg" width="600" height="265" /></picture></a><figcaption>Over 150ms spent in modernizr.js - on a Core i7 processor</figcaption></figure>
<p>Note that the bulk of the time spent on this script is in one function - s.csstransforms3d. Following the stack you can see that this function causes a style recalculation.
A quick bit of digging into the Modernizr source identifies the cause. The script injects an element and then uses functions offsetLeft and offsetHeight to test whether a CSS 3D transform has worked on the injected element:</p>
<pre class="language-js"><code class="language-js">tests<span class="token punctuation">[</span><span class="token string">'csstransforms3d'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">var</span> ret <span class="token operator">=</span> <span class="token operator">!</span><span class="token operator">!</span><span class="token function">testPropsAll</span><span class="token punctuation">(</span><span class="token string">'perspective'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span> ret <span class="token operator">&&</span> <span class="token string">'webkitPerspective'</span> <span class="token keyword">in</span> docElement<span class="token punctuation">.</span>style <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token function">injectElementWithStyles</span><span class="token punctuation">(</span><span class="token string">'@media (transform-3d),(-webkit-transform-3d){\#modernizr{left:9px;position:absolute;height:3px;}}'</span><span class="token punctuation">,</span><br /> <span class="token keyword">function</span><span class="token punctuation">(</span> <span class="token parameter">node<span class="token punctuation">,</span> rule</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> ret <span class="token operator">=</span> node<span class="token punctuation">.</span>offsetLeft <span class="token operator">===</span> <span class="token number">9</span> <span class="token operator">&&</span> node<span class="token punctuation">.</span>offsetHeight <span class="token operator">===</span> <span class="token number">3</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> ret<span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>This process makes sense - these offest function calls are a predictable way to test for the size of a DOM element on screen.
In order to calculate the size of the injected element these functions trigger a <a href="https://developers.google.com/speed/articles/reflow?hl=en">reflow</a>, in order for the browser to tell you the true size of an element it must ensure that all DOM manipulations (such as 3D transforms) have been executed.
Causing a reflow is generally considered bad for web performance, Stoyan Stefanov wrote a <a href="http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/">great post</a> on this way back in 2009.</p>
<p>Now, in order for Modernizr to do its thing properly it must load early. Here's what the <a href="http://modernizr.com/docs/">Modernizr documentation</a> says:</p>
<blockquote>
<p>The reason we recommend placing Modernizr in the head is two-fold: the HTML5 Shiv (that enables HTML5 elements in IE) must execute before the <code><body></code>, and if you're using any of the CSS classes that Modernizr adds, you'll want to prevent a FOUC.</p>
</blockquote>
<p>Ideally it should load before the other Javascript and CSS on the page to ensure that the correct features and styles are applied.
That means that the reflow behaviour is called twice - once for offsetLeft and once for offsetHeight - very early in the page load.
This is <em>bad</em> for render performance, and it is <em>really bad</em> on mobile where reflows (and the CSS 3D transform itself) are expensive in CPU time.</p>
<p>So, what do we do about it? First off, check if your Modernizr build runs the CSS 3D transforms check. If it does, make sure it is necessary and if not, remove it from your build using the Modernizr <a href="http://modernizr.com/download/">production download tool</a></p>
<p>Of course we need to make sure that we profile all of the Javascript on our sites, first-party and third-party.
We need to do this for every release, and every time a third-party changes.</p>
The Future of Web Performance - Part 12015-08-07T23:00:00Zhttps://simonhearne.com/2015/future-of-web-performance-1/<p>Web performance is <a href="https://simonhearne.com/2015/web-performance-optimisation-basics/">critical</a> to a successful online business.
Keeping on top of the latest technologies, techniques and practices allow us to stay ahead of the curve and get the edge over our competition.
This competitive edge comes through improved user experience and reduced operational costs (and the pride in achieving a <a href="http://www.sitepoint.com/speed-index-measuring-page-load-time-different-way/">sub-1,000 Speed Index!</a>)</p>
<p>New technologies are emerging which will change the face of web performance forever.
These technologies range from the underlying transport mechanism for our transmitted bytes to new javascript frameworks and an ever-growing range of browsing devices.
This complex and rapidly changing environment makes it harder than ever to plan and manage web performance.
In this short series I've attempted to summarise the key upcoming issues and describe how each one may impact your day-to-day life as a stakeholder in an online business.
I've also estimated when you need to really start thinking about it:</p>
<ol>
<li>Now..</li>
</ol>
<ul>
<li><a href="https://simonhearne.com/2015/future-of-web-performance-1/#noflash">No Flash!</a></li>
<li><a href="https://simonhearne.com/2015/future-of-web-performance-1/#https">HTTPS Everywhere</a></li>
<li><a href="https://simonhearne.com/2015/future-of-web-performance-1/#responsive">Responsive Images</a></li>
<li><a href="https://simonhearne.com/2015/future-of-web-performance-1/#mobile">Mobile-First</a></li>
<li><a href="https://simonhearne.com/2015/future-of-web-performance-1/#micro">Micro-Services and Containers</a></li>
</ul>
<ol start="2">
<li>Very Soon...</li>
</ol>
<ul>
<li>HTTP/2</li>
<li><em>More</em> Javascript?</li>
<li>Third-Party Functionality</li>
<li>Emerging Markets</li>
<li>Offline-First</li>
</ul>
<ol start="3">
<li>Soon...</li>
</ol>
<ul>
<li>ECMAScript 6</li>
<li>Low-Powered Devices</li>
<li>Social-First</li>
<li>Rich Content</li>
<li>Web 3.0 / Semantic Web</li>
</ul>
<h2 id="no-flash" tabindex="-1"><a name="noflash"></a>No Flash! <a class="direct-link" href="https://simonhearne.com/2015/future-of-web-performance-1/#no-flash" aria-hidden="true">#</a></h2>
<p>Flash has been around forever (well <a href="https://en.wikipedia.org/wiki/Adobe_Flash#Macromedia">10 years</a> is forever in web-years)
It allowed web designers to create rich experiences and port them directly to the web in a format compatible with most operating systems and browsers.</p>
<p><em>This. Was. Great.</em></p>
<p>So you want to embed a game on a page? Use Flash. So you want to have an interactive museum exhibit? Flash.
The uses were nearly endless, from poorly designed website introduction animations right through to online music albums.</p>
<p>The reason Flash was so universal is that you had the core code installed on your machine, the website element was just to tell the code where to download the Flash file from and how to render it.
This opens the client computer up to <a href="http://www.theguardian.com/technology/2015/jul/08/warning-adobe-flash-vulnerability-hacking-team-leak">quite</a> a <a href="http://blog.trendmicro.com/trendlabs-security-intelligence/analyzing-cve-2015-0311-flash-zero-day-vulnerability/">few</a> <a href="http://www.computerweekly.com/news/4500248673/Adobe-patches-Flash-Player-vulnerability-CVE-2015-3113">security</a> risks.
Files from the web run directly in native code installed on your machine is generally a bad thing.</p>
<p>Flash is actually no longer available on mobile devices and has not been developed <a href="http://www.techhive.com/article/258574/adobe_says_no_flash_player_for_android_41_plans_to_withdraw_app_on_aug_15.html">since 2012</a>, making it pretty useless in a mobile-first world.
HTML5 offers us much more secure and flexible ways of rendering dynamic content, such as audio and video, on web pages.
CSS and Javascript offer the power to make even more interactive web experiences.
All of these together mean that Flash is dead.</p>
<p>Losing Flash is a positive change. Flash was <a href="https://www.apple.com/hotnews/thoughts-on-flash/">horrible for web performance</a> and the modern replacements are more than up to the challenge.
The HTML5 and interactive client-side applications that replace Flash have their own performance challenges, however... (A story for another blog post or ten).</p>
<h2 id="https-everywhere" tabindex="-1"><a name="https"></a>HTTPS Everywhere <a class="direct-link" href="https://simonhearne.com/2015/future-of-web-performance-1/#https-everywhere" aria-hidden="true">#</a></h2>
<p>The Electronic Frontier Foundation created a browser plugin called <a href="https://www.eff.org/Https-everywhere">HTTPS Everywhere</a> in 2011 and it has been in active development <a href="https://github.com/EFForg/https-everywhere/graphs/contributors">since 2012</a>
The plugin will always try to redirect to a secure version of a site and its third-party resources. The intention of the plugin is to increase the security of your connections and to keep your data <a href="http://mashable.com/2011/05/31/https-web-security/" title="Web Security: Why You Should Always Use HTTPS">private across the network</a>
Google Chrome is planning to mark non-HTTPS pages as <a href="https://www.chromium.org/Home/chromium-security/marking-http-as-non-secure">affirmatively non-secure in the future</a>
This means that, as a site owner, you should expect all traffic to be served securely over HTTPS (oh, and you must <a href="https://tools.ietf.org/html/rfc7568">use TLS not SSL</a>)
This paradigm shift has significant performance implications as secure connections take time to establish.
To optimise secure connections ensure that HTTP keep-alive is enabled (the default for HTTP/1.1), that you have <a href="http://chimera.labs.oreilly.com/books/1230000000545/ch04.html#_session_tickets">session ticketing</a> enabled and that your negotiating server is optimised for rapid negotiation.
Ilya Grigorik runs the site <a href="https://istlsfastyet.com/">isTLSfastyet.com</a> which has much more information on the topic (hint: the answer is yes, TLS is fast when done right!)</p>
<h2 id="responsive-images" tabindex="-1"><a name="responsive"></a>Responsive Images <a class="direct-link" href="https://simonhearne.com/2015/future-of-web-performance-1/#responsive-images" aria-hidden="true">#</a></h2>
<p>Delivering content to multiple device-types <em>well</em> is <em>hard</em>.</p>
<p>Delivering content to mutliple device-types <em>fast</em> is <em>really hard</em>.</p>
<p>We all know that <a href="http://qz.com/393553/the-desktop-is-dying-and-mobile-is-winning-in-news-like-everything-else/">desktop is dying</a>, giving way to mobile and tablet consumers (<a href="http://internetretailing.net/2015/07/consumers-to-spend-9-3bn-shopping-on-mobile-while-they-commute-study-finds/" title="Consumers to spend £9.3bn shopping on mobile while they commute">especially while commuting</a>)
As images make up <a href="http://httparchive.org/interesting.php#bytesperpage">over 50%</a> of the average site, and bandwidth is limited with mobile connectivity, delivering a fast responsive site to all users means sending the smallest images possible whilst maintaining visual quality.</p>
<p>This is by no means an easy task, otherwise companies would not be <a href="https://www.resrc.it/">selling services</a> specifically to make it easier.
Fortunately for us, new HTML specifications are [being drawn up](<a href="http://responsiveimages.org/">http://responsiveimages.org/</a> Responsive Image Working Group) and <a href="http://caniuse.com/#feat=srcset" title="CanIUse.com / SrcSet">implemented in browsers</a> which will help in our quest.
These new specifications allow us to define images with breakpoints, just <a href="https://css-tricks.com/css-media-queries/">like our CSS</a>, so that smaller devices get the correct size images and retina displays get their high resolution versions.
See the <a href="http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-srcset">srcset definition</a> for a good place to start, and a useful explanation on <a href="https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-srcset/">CSS-tricks</a></p>
<p>Unfortunately for us, this creates more work in creating the multiple image versions on top of the additional markup required in our HTML.</p>
<p>Hey, I didn't say we were getting a free ride.</p>
<p>Some build and automation tools will make this process easier for you, however. See the <a href="https://github.com/andismith/grunt-responsive-images">Grunt Responsive Images</a> plugin (thanks to <a href="https://twitter.com/andismith" title="Andi Smith on Twitter">Andi Smith</a> as an example of responsive image automation.</p>
<p>If this all seems a bit much, your CDN or content delivery provider may be able to do it for you with little work on your end. Whatever happens, we need to keep marketing happy by delivering the best quality images, keep development happy by automating everything and keep IT happy by minimising cost of delivery.</p>
<p>This is not a small task!</p>
<h2 id="mobile-first" tabindex="-1"><a name="mobile"></a>Mobile-First <a class="direct-link" href="https://simonhearne.com/2015/future-of-web-performance-1/#mobile-first" aria-hidden="true">#</a></h2>
<p>I've mentioned earlier in this post that consumers are moving towards mobile devices.
This shift in consumer behaviour is also causing a shift in the way we build our websites.</p>
<p>Mobile-first means developing your site to work on mobile devices, then scaling the design up to larger screens.
This is as opposed to traditional responsive design which tends to wrangle desktop sites into smaller form-factors.</p>
<p>Being mobile-first means opening up your potential audience to the next <em>billion</em> online consumers.
This point was made extremely well by <a href="https://twitter.com/brucel" title="Bruce Lawson on Twitter">Bruce Lawson</a> in <a href="https://www.youtube.com/watch?v=BHO70H9tvqo">his keynote</a> at Velocity Santa Clara 2015.
Potential consumers in under-privileged areas <em>will</em> access your web site or application using a mobile device. Period.</p>
<p>Beyond the obvious advantages in being mobile-friendly, all types of online business will have customers following links from social media on their mobile phones.
Unfortunately, these types of clicks generally use the webview built in to the social application rather than a 'proper' web browser.
This means that there are even more platform / device / browser combinations to worry about and further considerations on web performance (which cache will I use? What resources will I get?)</p>
<p>Designing for mobile-first means layouts that start small and grow with the screen, minimal network interactions and use of key HTML5 features such as <a href="http://ogp.me/">Open Graph</a></p>
<p>Designing for <em>fast</em> mobile-first means:</p>
<ul>
<li>Heavy use of caching</li>
<li>Optimal use of network time</li>
<li>Removing <em>all</em> unnecessary third-party assets</li>
<li>Removing (or at least profiling) Javascript - no event handlers on touch or scroll please!</li>
<li>Fitting first-render into one round-trip - that means critical CSS is inline, with cacheable CSS added with Javascript</li>
</ul>
<h2 id="microservices-and-containers" tabindex="-1"><a name="micro"></a>Microservices and Containers <a class="direct-link" href="https://simonhearne.com/2015/future-of-web-performance-1/#microservices-and-containers" aria-hidden="true">#</a></h2>
<p>Containers and Microservices are real buzz-words at the moment.
If you are unfamiliar with the terms have quick read of <a href="https://medium.com/aws-activate-startup-blog/using-containers-to-build-a-microservices-architecture-6e1b8bacb7d1">this Medium article</a> on the topic.</p>
<p>Breaking a monolithic application into atomic components - each with their own function and service definition - is a very logical move.
Microservices have many benefits:</p>
<ul>
<li>Rapid development cycles - rapidly iterate services on fixed service definitions</li>
<li>Easy replacement of functionality - changing payment provider? Replace the payment service</li>
<li>Simple testing during development - stubbing of services means that each can be tested for performance and functionality in isolation</li>
<li>Logical scalability - scale each component as necessary rather than the whole application</li>
</ul>
<p>There's no escaping microservices. The concept offers many benefits and will apparently make web application development much more simple.</p>
<p>There are various performance and security issues that we need to be aware of though.
I won't try to enumerate all of the potential issues that microservices create - because <a href="https://twitter.com/garethr" title="Gareth Rushgrove on Twitter">Gareth Rushgrove</a> did it better than I could ever do at the most recent <a href="http://www.meetup.com/London-Web-Performance-Group/">London Web Performance Meetup</a>
Please find his fantastic slides <a href="https://speakerdeck.com/garethr/containers-and-microservices-make-performance-worse">here</a></p>
<p>Wow, that's quite a list of things we need to care about, and I haven't even started to consider the way we measure and monitor site performance.
Using <a href="https://webpagetest.org/">WebPageTest</a>, <a href="http://www.sitespeed.io/">sitespeed.io</a>, <a href="https://speedcurve.com/">SpeedCurve</a> or similar to test front-end performance before any release is <em>critical</em>.
We also need to ensure that we include mobile and responsive breakpoint testing.</p>
Questions to Ask Your Third-Parties2015-07-03T13:17:00Zhttps://simonhearne.com/2015/questions-to-ask-your-third-parties/<p>We've discussed how to <a href="http://simonhearne.com/2015/find-third-party-assets/">find the third-party assets</a> on your site and started to look at how to <a href="http://simonhearne.com/2015/manage-3p-risk-csp/">mitigate the risk</a> that they pose.
But what should you do when you add new third-parties to the site?</p>
<p>There are some key questions you need to ask of the third-parties to ensure that they will not impact the user experience, performance or security of your customers.
In the best-case scenario you have the opportunity to ask these questions before signing a contract with the third-party in question.
In the more common case, someone else has already created a relationship and has just requested that the tag be added to your site.
Either way, having a list of questions to ask will give you an idea of how mature the third-party is and how much work will be required to ensure that your customers continue to have a great experience.</p>
<p><a href="http://jsmanners.com/">JSManners.com</a> by <a href="https://twitter.com/triblondon">@triblondon</a> was designed to address exactly this issue, giving a list of questions that you can ask a third-party to give them a score.
I'm a fan of scores, they're simple and easy to report on. For assessing third-parties, though, there are more qualitative assessments to make.
For example you may not be willing to use a third-party which includes jQuery outside of a protected scope (as the risk of breaking your site is too high) but you are willing to accept 1kB of cookie data on your host domain.
So I've borrowed the list of questions from JSManners and re-organised them into categories, I've also removed some and added others as well as adding some context to each point so that you can understand the reasoning for the question.</p>
<ul>
<li>
<p><strong>Critical Questions</strong></p>
<ul>
<li>Are common 3rd party libraries (e.g. jQuery, Underscore, Backbone) included outside of a protected scope (ie is there any chance they may conflict with other instances of the same library that may be included on the page? <em>- If common libraries conflict, it may break critical functionality of your site. Third-parties are on different release cycles to you so even if everything works now, it may not in the future.</em></li>
<li>Do component script(s) block render in any way? <em>- If a third-party asset blocks the render of your page(s), it is a single-point-of-failure (SPOF). Thus if your customers cannot download their script for any reason, it will take <a href="http://www.stevesouders.com/blog/2014/11/14/request-timeout/">at least 20 seconds</a> to render your page(s). You want to know about this and mitigate the risk (by asking all of the availability questions below!)</em></li>
<li>Do component script(s) use document.write? <em>- if so, you cannot load them asynchronously, every DOM element below the inserted script is <a href="http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/">blocked from rendering</a> while it downloads and it <a href="http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/">blocks other dynamic scripts</a>.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>Good Behaviour</strong></p>
<ul>
<li>How many objects/variables do component script(s) require in / add to the global scope?</li>
<li>Do component script(s) produce any console output in normal operation? <em>- they shouldn't; if they do, why and what content is output?</em></li>
<li>Do component script(s) cause any JavaScript errors in normal operation? <em>- they shouldn't; if they do, why and what content is output?</em></li>
<li>Can component script(s) be loaded asynchronously? <em>- ideally scripts should be loaded asynchronously to prevent potential impact on your <a href="https://css-tricks.com/thinking-async/">users' experience</a>.</em></li>
<li>How many (infinitely and unconditionally recurring) timer events does the script fire per minute? <em>- timer events such as setInterval() can cause the main browser thread to lock up in some circumstances and can cause performance issues if the functions access the DOM or perform network actions.</em></li>
<li>If the script tag is removed from the page (e.g. because it appears to be causing problems), what is the UI impact (on subsequent page loads)? <em>- if you have a method of removing badly-behaving scripts, you should ensure that the customer experience is not negatively impacted. This may be relevant for a third-party that adds content to the page such as adverts or personalisation.</em></li>
<li>Which browsers is the component script tested in? <em>- if the third-party can't guarantee that the script will behave well in all of the browsers you support then you can't guarantee your user experience.</em></li>
<li>When does the script interact with the DOM? <em>- if the script interacts with the DOM on regular, critical or unpredictable events there may be a significant performance impact.</em>
<ul>
<li>On parse</li>
<li>On the DOMReady event</li>
<li>On the load event</li>
<li>On recurring timer events</li>
<li>On user interaction events</li>
<li>On network related events</li>
<li>When invoked by a function call on JS API</li>
</ul>
</li>
</ul>
<br />
</li>
<li>
<p><strong>Local Storage</strong></p>
<ul>
<li>Does the script set any persistent cookies on the host domain? <em>- this may be bad, I've seen a customer site which sends 200kB of upstream cookie data on each page load because of third-party cookies set on the host domain.</em></li>
<li>Does the component depend on, or modify, any existing cookies set by the host page? <em>- it shouldn't, these are your cookies.</em></li>
<li>What is the maximum total size of data, in bytes, stored in cookies by the script? <em>- depending on the domain, a large amount of upstream cookies can cause performance issues. Remember that cookies cannot be compressed on the network (at least with HTTP/1.1).</em></li>
<li>Does the component persist data in the browser using a mechanism other than cookies? <em>- you should be aware if the third-party relies on localstorage or equivalent and ensure that there are no security or performance risks associated with it.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>DOM Interaction</strong></p>
<ul>
<li>If the component adds event listeners to DOM elements, does it remove them when they're no longer required?</li>
</ul>
<br />
</li>
<li>
<p><strong>Content Delivery</strong></p>
<ul>
<li>Is the script (including all non-dynamic resources) cached on a CDN with global reach? <em>- using a CDN ensures that the resources are delivered to your customers as quickly as possible and shows that the third-party understands the importance of performance.</em></li>
<li>Where are there servers that can accept write operations performed by the script? <em>- if the browser needs to send data back to the third-party, having locations close to your customers ensures best possible performance and that data won't be lost due to poor connections.</em></li>
<li>What is the total data size in kilobytes transferred on page load? <em>- large amounts of Javascript can cause performance issues, ensuring that the third-party only delivers necessary content.</em></li>
<li>What is the minimum browser-cache TTL (in seconds) of files downloaded when the component script loads (ie. Cache-control max-age)? <em>- having good cache control on third-party assets reduces the amount of network time for repeat visits, however some third-parties will disable all caching as the scripts are dynamic.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>Analytics</strong></p>
<ul>
<li>Do the component script and all associated resources provide ResourceTiming <a href="http://www.w3.org/TR/resource-timing/#cross-origin-resources">opt-in header</a> (ie. <a href="https://nccgroup.webex.com/mw0401lsp13/mywebex/default.do?service=1&siteurl=nccgroup&nomenu=true&main_url=%2Fmc0901lsp13%2Fe.do%3Fsiteurl%3Dnccgroup%26AT%3DMI%26EventID%3D349336237%26UID%3D504506122%26Host%3DQUhTSwAAAAJ8Y2XjDvwHvaWR52DTDaT-zR08KLPczSY835Z46AfLT52IErW9zlPt5lkk-nSo-xT6NY8Q5oCfFw2U0-39b8pJ0%26FrameSet%3D2%26MTID%3Dm4dad1c514e54409238c2142cd843ae2d">Timing-Allow-Origin</a>: *)? <em>- if you are using a real user monitoring (RUM) solution, this header allows you to measure and record detailed timings of your third-party resources. There may be security implications here though.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>Availability</strong></p>
<ul>
<li>Is the script or any web service it depends on served from a single origin (ie. is there just one origin data center)? <em>- this could be a single-point-of-failure for the third-party.</em></li>
<li>In the event of a complete failure of the primary origin (eg unmitigated data centre power failure), what is the mean time to recovery (MTTR), in seconds? <em>- this checks that the third-party are aware of the consequences of the origin becoming available.</em></li>
<li>How many full outages has the service experienced in the 12 months prior to today? (where full outage means a user with empty cache anywhere in the world being unable to use the service) <em>- this gives you an idea of how reliable the service will be. Based on the impact of the service being unavailable this could be a deal-breaker.</em></li>
<li>Is there a public status page describing the current operational state of the script's backing services? <em>- if the third-party is open about uptime and performance this serves as a good indicator of their maturity.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>Transport</strong></p>
<ul>
<li>What is the minimum number of HTTP requests that the script will make when invoked? <em>- if this number is high it suggests an inefficient design, creating more network traffic than is ideal.</em></li>
<li>Is it possible to make ALL the script's network activity go over HTTPS? <em>- your customers trust you with their information, third-parties become potential leaks of this information and HTTPS helps to reduce the risk.</em></li>
<li>Is HTTP 2.0 supported for all requests? <em>- HTTP 2.0 is the newest HTTP protocol which enables more efficient communications. In most browsers it is only enabled over HTTPS.</em></li>
<li>Can the script's initial payload be bundled with the host page's own scripts? <em>- this reduces the network overhead and delay of the initial request. It can also mitigate the risk of the third-party being unavailable and keeps the core code under your control, meaning that you manage the new third-party code releases to your site.</em></li>
</ul>
<br />
</li>
<li>
<p><strong>Security</strong></p>
<ul>
<li>Will the component be involved with collecting user identifiable data (including any one of name, email address, credit card details, or phone number)? <em>- any of this can be risky and may be against the law in some of your customers' countries or open you up to PCI compliance issues.</em></li>
<li>For European customers, does the script's backing service either a) not store any personal data, b) store personal data only within the EU, or c) store personal data outside the EU in compliance with the US Safe Harbour data protection standard? <em>- you should ensure that your third-parties are not putting your customers' data at risk, pursuant to your (and your customers') local and international laws.</em></li>
<li>Does the script respect the Do-Not-Track HTTP header, either actively or by not conducting any tracking at all? <em>- Do-Not-Track is a <a href="https://en.wikipedia.org/wiki/Do_Not_Track">proposed HTTP header</a> which suggests that customers can opt-out of cross-site behaviour tracking. If your third-party respects the DNT header then they have respect for your customers' privacy wishes.</em></li>
</ul>
</li>
</ul>
Manage Third-party Risk Using a Content Security Policy2015-07-03T13:17:00Zhttps://simonhearne.com/2015/manage-3p-risk-csp/<p>Andy Davies and I have <a href="https://www.youtube.com/watch?v=9OtUOgx0cZg" title="What Are Third-party Components Doing to Your Site?">spoken</a> at a number of conferences about the risks that third-party components can have on your website.
In these talks we try to illustrate the fact that third-party components can have all sorts of negative consequences on your site's availability and performance.
At the end of our talks we are always asked the same thing: 'what can we do about it?'.</p>
<p>Once you've got your third-parties under control, set up a tag management solution and identified the business value you will still have some third-parties on your site.
What can we do to mitigate the risk that those critical third-parties pose?
There are a number of methods to minimise the risk, such as loading the third-party scripts with an <a href="https://www.youtube.com/watch?v=I5uhZcJ30SA" title="Guy Podjarny talks about Third-party Performance">async or defer tag</a> (after ensuring that the third-party script <a href="http://www.w3.org/TR/html401/interact/scripts.html#adef-defer" title="W3C Defer Specification">doesn't use document.write()</a>!)
The problem is that these third-party scripts can still make calls to fourth- and fifth- and sixth- ... parties, which can multiply the risk posed to your site.
We know this through using tools such as <a href="http://blog.simonhearne.com/portfolio/request-map/" title="Request Map">request map</a> which highlights how requests to these third-parties are made.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/requestmap_very.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/VMIHBz-MYG-300.avif 300w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/VMIHBz-MYG-300.webp 300w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Third-party calls can become complex and hard to manage" loading="lazy" decoding="async" src="https://simonhearne.com/img/VMIHBz-MYG-300.jpeg" width="300" height="243" /></picture></a><figcaption>Third-party calls can become complex and hard to manage</figcaption></figure>
<p>We don't currently have a good way to prevent these nth-party calls. Or at least not that people are using in the wild.
Content-Security-Policy is a proposed HTTP Header which prevents cross-site scripting (XSS) attacks. It does this by defining whitelists of domains which can load scripts, styles images etc. from.
This prevents resources being requested from malicious domains <em>at the client-side</em>.
CSP is <a href="http://caniuse.com/#feat=contentsecuritypolicy" title="Can I use... CSP">pretty widely supported</a> amongst the major browsers and as it is just an HTTP header it can be set by any server-side language, any web server and any CDN.
Using your CDN would allow you to define geographic-specific third-party script whitelists which would be great to prevent calls to social networks from inside China, as an example.
I've built a quick example to demonstrate the concept <a href="http://simonhearne.co.uk/sandbox/csp/" title="Console message when unexpected third-party asset is blocked">on my site</a>:</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/CSP.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/p8otYhuZiH-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/p8otYhuZiH-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Console message when unexpected third-party asset is blocked" loading="lazy" decoding="async" src="https://simonhearne.com/img/p8otYhuZiH-600.jpeg" width="600" height="50" /></picture></a><figcaption>Console message when unexpected third-party asset is blocked</figcaption></figure>
<p>CSP will beacon back JSON every time a resource is blocked, it also has a <a href="http://www.w3.org/TR/CSP/#content-security-policy-report-only-header-field" title="W3C CSP Report-only Mode">report-only feature</a>, where the request still happens at the client but it would allow you to report on unknown third-parties for further analysis.
A potential secondary benefit is that you can define the protocol in the CSP - thus enforcing HTTPS on the requests and better respecting your customers' privacy.</p>
<p>I can see a future where tag managers will integrate with CDNs (through <a href="https://docs.fastly.com/api/" title="Fastly API Docs">great APIs</a>) and will automatically create CSPs based on the tags that are loaded for each page and geography.
Wouldn't that be nice! Until then, though, it will take some manual intervention to get CSPs up-and-running.</p>
<p>Would you use this to restrict the third-parties on your site or is CSP too extreme for your needs? Please let me know if you try it out in the wild!</p>
Using a Web Performance Heatmap to Assess Page Performance2015-05-29T15:30:00Zhttps://simonhearne.com/2015/performance-heatmap/<p><a href="http://heatmap.webperf.tools/" class="btn btn-block btn-primary">Go to the tool</a></p>
<p>I've recently had a few clients ask if there is a way to test when certain parts of their pages become visually complete. Imagine a media company that relies on advertising revenue who wants to measure when the above-the-fold adverts are rendered, relative to the actual content of the page.</p>
<p>We have a few ways to attempt this, for example <a href="http://www.html5rocks.com/en/tutorials/webperformance/usertiming/" title="HTML5Rocks User Timings Tutorial">user timings</a> can add a performance timing marker based on an element loading (note that this can't tell when the loaded element / image is actually rendered). To really tell when an element is rendered we either need to dig in to <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool/trace-event-reading" title="Chromium about:tracing page">paint traces</a> or use a filmstrip view from e.g. <a href="http://webpagetest.org/" title="WebPageTest.org">webpagetest</a>.</p>
<p>The only realistic solution to tell when an element on a page <em>actually</em> renders in a controlled environment is to use a filmstrip view and scroll through until the relevent part of the page is visible. This is obviously a labour-intensive task and tricky to automate.</p>
<p>Thinking about better solutions to this challenge led me to imagine a heatmap of a webpage - showing in red-amber-green how late each pixel was to load. This would make it simple to tell, at a glance, what parts of the page are slow or fast to render.</p>
<p>To test this theory out I started running filmstrips from WebPageTest through PHP to compare pixels to the final frame and determine when each pixel achieved its final state. Doing this I discovered that by default WebPageTest filmstrip images are scaled and heavily compressed. While to remove the scaling you have to re-build the WebPageTest agent, you can disable image compression <a href="http://jrvis.com/blog/wpt-screenshots/">using a hidden form field</a>. Now that I had uncompressed images I could write a function that iterates through the frames and notes at what load time each pixel completes. This matrix can then be used to generate a heatmap layer based on the performance budget (e.g. if a pixel acheives final state at 8 seconds and the budget is 6 seconds, colour the pixel red).</p>
<p>I ended up building a wrapper on top of WebPageTest to automate this process and generate a heatmap for you: <a href="http://heatmap.webperf.tools/" title="Web Performance Heatmap">heatmap.webperf.tools</a>.</p>
<figure class=" clickable"><a href="https://simonhearne.com/images/heatmap_pinterest.png"><picture><source type="image/avif" srcset="https://simonhearne.com/img/Vv8sBhnDvX-600.avif 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><source type="image/webp" srcset="https://simonhearne.com/img/Vv8sBhnDvX-600.webp 600w" sizes="(max-width: 1024px) 90vw, (max-width: 1240px) 75vw, 810px" /><img alt="Pinterest Heatmap highlighting that the top images are late to load" loading="lazy" decoding="async" src="https://simonhearne.com/img/Vv8sBhnDvX-600.jpeg" width="600" height="328" /></picture></a><figcaption>Pinterest Heatmap highlighting that the top images are late to load</figcaption></figure>
<p>So go ahead - have a play. Note that it is a proof-of-concept toy not a complete tool so please let me know what you think.</p>
<p><em>You may notice some strange results when you test your website. Many websites have rotating carousels or cookie / advert banners which push all of the page content down, this causes the heatmap to reflect potentially poor results even though content has been rendered earlier. My opinion on this is that shifting the page down for a cookie / advert banner is bad for user experience. <a href="http://shouldiuseacarousel.com/" title="Should I Use a Carousel?">So is using a rotating carousel</a>.</em></p>
How to Find the Third-Parties on Your Site2015-03-19T13:32:40Zhttps://simonhearne.com/2015/find-third-party-assets/<p><a href="http://requestmap.webperf.tools/" class="btn btn-block btn-primary">Go to the tool</a></p>
<p>My customers often ask how a third-party asset got on their site. A director of digital once <strong><em>told</em></strong> me that Facebook was not on their site, not at all.</p>
<p>I ran the homepage through WebPageTest and sure enough, there were a bunch of calls to various subdomains of <a href="http://facebook.com/">facebook.com</a>. Thankfully WebPageTest stores initiator, referer and redirect headers; so with a little work you can find out where these third-party calls come from. The director was correct, there were no calls to Facebook on their site. It was a third-party creating <em>fourth-party</em> calls to Facebook! This can have serious ramifications if the Facebook (or <em>any other</em> third-party call) affects customer experience. Radware explain this well in an <a href="http://www.webperformancetoday.com/2011/10/13/how-vulnerable-is-your-site-to-third-party-failure/">article from 2011</a></p>
<p>Trawling through the initiators and referers was a headache so I wrote a tool to do it: <a href="http://requestmap.webperf.tools/" title="Request Map">requestmap.webperf.tools</a></p>
<p>The request map runs a test in Chrome on public WebPageTest and uses the initiator, referer and redirect data to work out where every request comes from. Here's the result when you run <a href="http://google.co.uk/">Google.co.uk</a> through the tool:<img src="https://simonhearne.com/images/requestmap_google.png" alt="ninja_map" /></p>
<p>From the picture I can see immediately that the bulk of the content on the page comes from <a href="http://www.google.co.uk/">www.google.co.uk</a> (size of blob is proportional to percentage of total bytes).
There is a redirect from <a href="http://google.co.uk/">google.co.uk</a> -> <a href="http://www.google.co.uk/">www.google.co.uk</a> and a number of requests from other Google-owned domains.</p>
<p>It all looks pretty simple on <a href="http://google.co.uk/">Google.co.uk</a>, there's basically nothing on it. But when it comes to an ecommerce site or a site generating advertising revenue, third-parties are everywhere.
Whether they are for analytics, advertising, tracking, attribution... third-parties make people <em>money</em>.
As such, they tend to be added to sites without much consideration for management of the relationship, the technology and (dare I say it) third-party SLAs.</p>
<p>Take the <a href="http://answers.com/">answers.com</a> homepage as an example:</p>
<p><a href="http://requestmap.webperf.tools/render/150319_RK_58a22e3fe46f36365ab817e2b03f3606"><img src="https://simonhearne.com/images/requestmap_img.png" alt="answers_map" /></a></p>
<p>Here you can see that the root domain (<a href="http://www.answers.com/">www.answers.com</a>) makes up a small proportion of the total page weight. This site uses domain sharding across a number of CDN domains (in yellow) to maximise performance for high-bandwidth customers. One CDN subdomain stands out from the others though: <a href="http://rxf3.answcdn.com/">rxf3.answcdn.com</a> is the big yellow blob on the right. Almost 500kB of content comes from this domain but, more interestingly, it initiates calls to Google and Facebook which then spawn requests to another six domains!</p>
<p>Another observation is based on the length of the edges on the map (the lines which join the blobs), these are proportional to the mean response time of that domain. Blobs that are far away from the target site on the map are far away from the customer's experience. A single asset from <a href="http://www.dsply.com/">www.dsply.com</a> (far left) took over 1.1s to load less than one kilobyte of content! It also took almost a second to load two kilobytes from <a href="http://snip.answers.com/">snip.answers.com</a>. Are these assets critical to the user experience? If so, there's a massive performance hit here.</p>
<p>There's a lot of work to do on analysing third-party components but visualising who is on your site (and how they got there) is a good start!</p>
Web Performance Optimisation Basics2015-03-11T19:54:00Zhttps://simonhearne.com/2015/web-performance-optimisation-basics/<p>Our web performance industry was unquestionably seeded by Steve Souders' book <a href="http://shop.oreilly.com/product/9780596529307.do">High Performance Websites</a>. In his book, steve identifies <a href="http://stevesouders.com/hpws/rules.php">14 rules</a> to improve web perfomance.</p>
<p>As part of my job at NCC Group I get to produce optimisation reports for our customers (called healthchecks). In these reports we tend to focus on five key areas, one of these is front-end optimisation. This is where Steve's rules come in. The rules were first published in 2007, over 8 years ago! In the fast-paced world of technology this is a huge amount of time, although admittedly the technology that defines the web has not changed too much since then. Web browsers and business attitudes to web performance have changed, however. Companies are focusing more effort and resource on ensuring a good online experience for their customers. Gradually, web performance is being considered as one of the key factors in the customer experience.</p>
<p>What we need now is a concise, logical list of technical recommendations to improve website performance.</p>
<p>The list needs to be logical in terms of implementation and importance, but must also have business context in terms of complexity of implementation. Generally, improving front-end performance reduces operational cost: fewer bytes over a CDN is less $ spent, fewer connections to a firewall is less utilisation. There is also an associated cost - implementing combined CSS involves process changes, new technologies etc.</p>
<p>The challenge is finding a generic enough list of recommendations which will apply across website industries, customer types, mobile v. desktop etc. I have tried to create a list which covers these elements but only caters for browser activity before the onLoad event (according to <a href="http://en.wikipedia.org/wiki/DOM_events">wikipedia</a> the onLoad event "fires when the <a href="http://en.wikipedia.org/wiki/User_agent" title="User agent">user agent</a> finishes loading all content within a document, including window, frames, objects and images"). Content requested after onLoad <em>should not</em> impact customer experience - this is the realm for analytics, tracking, advertising etc. Here's my current working draft of pre-onLoad recommendations:</p>
<ul>
<li>Make fewer requests
<ul>
<li>Combine CSS</li>
<li>Combine JS</li>
<li>Sprite small images</li>
<li>Cache everything (no ETags?)</li>
</ul>
</li>
<li>Transmit fewer bytes
<ul>
<li>Optimise Images</li>
<li>Compress everything</li>
<li>Send only what you need (responsive images?)</li>
<li>Cache everything</li>
</ul>
</li>
<li>Reduce request latency
<ul>
<li>Optimise the application</li>
<li>Use asynchronous requests</li>
<li>Put content close to the consumer</li>
<li>Optimise headers</li>
<li>Use the latest protocols</li>
<li>Set an intelligent initial congestion window</li>
</ul>
</li>
<li>Prioritise critical content
<ul>
<li>Inline critical CSS</li>
<li>Inline critical images</li>
<li>Inline critical JS</li>
<li>Outline non-critical content</li>
<li>Load images asynchronously</li>
</ul>
</li>
<li>Remove single points of failure
<ul>
<li>Use async & defer</li>
<li>Manage third-parties (tag management?)</li>
<li>Re-consider <link src="" /> tags</li>
</ul>
</li>
</ul>
<p>The specific implementation of each recommendation is unimportant, but the general concepts and prioritisation are key to getting a website to perform at its best.</p>
<p>Feedback graciously welcomed!</p>
<p>Si.</p>