Jekyll2024-03-05T21:09:08+00:00https://nooshu.com/feed.xmlFrontend Web Developer, Oxfordshire, UK - Matt HobbsThe virtual notebook of Matt Hobbs, about web development and any other subject that interests him. UI / Frontend Developer.
Matt HobbsGOV.UK Cookie banner and why it “won’t go away”2023-12-12T09:00:00+00:002023-12-12T09:00:00+00:00https://nooshu.com/blog/2023/12/12/govuk-cookie-banner-browser-privacy-and-security<p>This is a blog post I’ve been meaning to write for many years. While working at Government Digital Service (GDS) for 6 years as Head of Frontend, I had so many people ask me about the GOV.UK Cookie banner. The main complaint being “it would never go away”. I was asked this question on so many platforms like email, Twitter, and even Reddit! The fact is, it has a surprisingly complex answer!</p>
<p>The aim of this blog post is to answer this question, so I can easily point someone in the direction of this post, should they be interested in the many technical details involved.</p>
<p>First and for-most, what is a cookie banner?</p>
<h2 id="what-is-a-cookie-banner">What is a cookie banner?</h2>
<p>In simple terms a cookie banner is that annoying pop-up that you get on every website on the modern web. The one that asks you to “accept” or “decline” the storage of cookies in your browser.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="591" class="figure__image" src="/images/cookie-banner/govuk-cookie-banner.jpg" alt="Image of the GOV.UK Cookie banner on a laptop computer." />
</picture>
<figcaption class="figure__caption">The latest GOV.UK cookie banner as seen on a laptop computer pushes down the content of the page.</figcaption>
</figure>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="207" class="figure__image" src="/images/cookie-banner/bbc-cookie-banner.jpg" alt="The BBC Home cookie banner" />
</picture>
<figcaption class="figure__caption">The BBC Home cookie banner that is anchored to the bottom of the page.</figcaption>
</figure>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="335" class="figure__image" src="/images/cookie-banner/vue-cinemas-cookie-banner.jpg" alt="Cookie banner for VUE Cinema UK." />
</picture>
<figcaption class="figure__caption">The cookie banner for VUE Cinemas UK floats at the bottom right of the page.</figcaption>
</figure>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="601" class="figure__image" src="/images/cookie-banner/cnn-cookie-banner.jpg" alt="Huge CNN cookie banner." />
</picture>
<figcaption class="figure__caption">The huge CNN cookie banner that is anchored to the bottom of the page and covers the main article content.</figcaption>
</figure>
<h3 id="why-do-cookie-banner-exist">Why do cookie banner exist?</h3>
<p>Cookie banners exist primarily due to the implementation of privacy regulations such as the <a href="https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guide-to-pecr/what-are-pecr/">Privacy and Electronic Communications Regulations (PECR)</a> in the European Union. There are similar regulations in other parts of the world like the <a href="https://www.oag.ca.gov/privacy/ccpa">California Consumer Privacy Act (CCPA)</a> in the United States.</p>
<h3 id="what-are-cookies">What are cookies?</h3>
<p>Cookies are small text files that store information on a user’s computer or device when they visit a website. They are used for various purposes. Some example usages are:</p>
<ul>
<li>remembering a user’s preferences</li>
<li>tracking user behaviour across websites</li>
<li>delivering targeted advertising</li>
</ul>
<h3 id="why-are-they-called-cookies">Why are they called “cookies”?</h3>
<p>The term “cookie” was coined by web-browser programmer <a href="https://en.wikipedia.org/wiki/Lou_Montulli">Lou Montulli</a> in 1994. Montulli was inspired by the concept of “<a href="https://en.wikipedia.org/wiki/Magic_cookie">magic cookies</a>” in early computer networking. These are small pieces of data that are sent between computers to help them communicate more efficiently. The term “magic cookie” was itself inspired by fortune cookies, which are small treats that contain a surprise message.</p>
<p>Montulli decided to use the term “cookie” for this new technology as he thought it would be easier for people to remember than a complex technical term.</p>
<p>Over time, the term “cookie” has become synonymous with any small piece of data that is stored on a user’s computer by a website.</p>
<h2 id="the-govuk-cookie-banner">The GOV.UK Cookie banner</h2>
<p>In this post, my focus is specifically GOV.UK (<a href="https://www.gov.uk">www.gov.uk</a>) and related UK Government services e.g.</p>
<ul>
<li><a href="https://www.universal-credit.service.gov.uk/">Universal Credit</a></li>
<li><a href="https://www.check-mot.service.gov.uk/">Check MOT</a></li>
<li><a href="https://www.access.service.gov.uk/login/signin/">HMRC Login</a></li>
<li><a href="https://www.sign-in.service.gov.uk/getting-started">One Login</a></li>
<li><a href="https://www.payments.service.gov.uk/">GOV.UK Pay</a></li>
<li><a href="https://www.notifications.service.gov.uk/">GOV.UK Notify</a></li>
</ul>
<p>What you may notice about the GOV.UK homepage (<a href="https://www.gov.uk">www.gov.uk</a>) and the services listed above, is that they all have the same design.</p>
<p>There are a few minor differences between pages, but the overall aim is to use the same design language across all services. This is why the <a href="https://design-system.service.gov.uk/">GOV.UK Design System</a> exists. To help service teams across the UK government achieve the same design quickly and easily.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="603" class="figure__image" src="/images/cookie-banner/design-system-page.jpg" alt="Image of the Design System Homepage" />
</picture>
<figcaption class="figure__caption">The GOV.UK Design System homepage with links to patterns and components for building a UK Government service.</figcaption>
</figure>
<p>It’s also worth mentioning that the GOV.UK Design System also has a <a href="https://design-system.service.gov.uk/components/cookie-banner/">cookie component</a> with lots of guidance for other service teams to use.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="930" class="figure__image" src="/images/cookie-banner/design-system-service-govuk-components-cookie-banner.jpg" alt="The design system has a dedicated cookie banner component." />
</picture>
<figcaption class="figure__caption">The GOV.UK Design System has a dedicated cookie banner component.</figcaption>
</figure>
<p>This consistency was a conscious decision by the design team <a href="https://gds.blog.gov.uk/2012/01/19/designing-govuk/">back in 2011–2012</a>. Its design is clean and simple and has always had accessibility & inclusivity as top priorities from the very beginning.</p>
<p>The design actually won the <a href="https://www.gov.uk/government/news/govuk-wins-design-of-the-year-2013">Design Museum Design of the Year Award 2013</a> and the <a href="https://www.dandad.org/awards/professional/2013/writing-for-design/20081/govuk/">D&AD 2013 Black Pencil award</a>.</p>
<p>But as with every bold decision in life there are people who disagree. Like <a href="https://gds.blog.gov.uk/2012/10/16/gov-uk-the-start/#comment-2454">this comment from Howard Andrews</a> who says:</p>
<blockquote>
<p>Where is the government web site for grown-ups? The whole of “gov.uk” is obviously designed for primary school children and those with special educational needs.</p>
</blockquote>
<p>I, personally, disagree with Howard, but as it’s taxpayer money being spent everyone is entitled to express their opinion.</p>
<p>The point I’m getting at is that consistency across the whole user journey was chosen to simplify the site for users. Do users need to know that they are moving between services run by entirely different departments? Not really! As long as they can complete their task quickly and easily and get on with their lives, I’m sure they’ll be happy.</p>
<p>But it’s worth noting that this design consistency across services is a double-edged sword:</p>
<p><strong>Pros</strong></p>
<ul>
<li>Gives users a stable design across the complete user journey thus reducing the cognitive load users experience.</li>
<li>Allows users to recognise a government website instantly.</li>
<li>Reduces development and design time in the public sector.</li>
<li>Reduces costs associated with building government services.</li>
<li>Builds brand loyalty and a site’s trustworthiness</li>
</ul>
<p><strong>Cons</strong></p>
<ul>
<li>Changing a design across multiple departments requires more coordination.</li>
<li>Technical edge-cases like the cookie banner reappearing across services (a lot more on this later!)</li>
<li>Users may be unaware they have navigated to an entirely different site.</li>
<li>Design stagnation due to a rigid set of rules to follow, the design may eventually look dated.</li>
</ul>
<p>I strongly believe the benefits of consistent branding far outweigh the drawbacks. Especially when it comes to government services that millions of people use every day.</p>
<h2 id="go-away-cookie-banner">Go away cookie banner!</h2>
<p>Of all the “cons” listed above, this is the one that users notice the most!</p>
<blockquote>
<p>Users may be unaware they have navigated to a completely different site</p>
</blockquote>
<p>If the site design barely changes between user navigations it’s no wonder a user still thinks they are on the same site. As mentioned before, this was an intentional decision. It abstracts away the complexity of government departments into a clear and consistent user journey.</p>
<p>There’s no single source of truth when it comes to back-end technologies in UK government services. For example, <code class="language-plaintext highlighter-rouge">www.gov.uk</code> is built using Ruby on Rails. Whereas Universal Credit uses Java. The DVSA’s Check MOT service is built using PHP. Unfortunately, the reason the cookie banner “won’t go away” is exactly <em>because</em> the user is on an entirely different site. There are technical reasons for why this happens that I will cover now.</p>
<h2 id="browser-privacy">Browser Privacy</h2>
<p>We start our technical explanation with how browsers have changed over recent years to protect users’ privacy online. Back when web browsers were first developed, the internet was nothing like it is today. User-data capture was primitive and so was internet advertising. Unfortunately, the same can’t be said about the “modern web” of 2023. Companies across the globe have realised how valuable user-data is. And a modern-day gold-rush has emerged from the access to billions of peoples private browser data. Companies like Facebook and Google were until very recently able to easily track users across the internet using specific information like:</p>
<ul>
<li>Websites visited</li>
<li>Search queries</li>
<li>Social media interactions</li>
<li>Location data</li>
<li>Device information</li>
</ul>
<p>From this data, companies can build an extremely accurate digital profile of users. Especially when these companies analyse this data all together (e.g. for all users). Very recently Google toyed with an idea called <a href="https://privacysandbox.com/proposals/floc/">Federated Learning of Cohorts (FloC)</a>.</p>
<p>FloC was a fairly simple idea. Examine all the captured user data and look for similarities in the data. Then create a bucket (or cohort) for all these similar profiles to live in. This turned out to be a great way to track a large group of people while keeping an individual’s identity private. For example, if you recently browsed websites related to motorsport (e.g. Formula 1), then you would be placed into a cohort of other people who are interested in motorsport.</p>
<p>This all sounds very innocent, but it’s incredible how accurate these cohorts can become, when there are 10s of thousands of people in them and millions of data points to examine. These cohorts would become more and more accurate over time as they gather more data and refine the algorithms. Eventually, these companies would have such an accurate idea of who you are and what you like. They’d be able to predict what you enjoy and target you with targetted adverts that they would be fairly certain you were likely to engage with. Thankfully, the privacy community pushed back on the idea of FloC and the development was stopped in favour of a new technology called <a href="https://privacysandbox.com/proposals/topics/">Topics</a>.</p>
<p>The key differences between FloC and Topics are:</p>
<ul>
<li>A user will be assigned to a maximum of 5 topics from a predefined list of 350.</li>
<li>Cohorts aren’t used, instead a user is assigned to specific topics based on their browsing history.</li>
<li>A user can edit the topics they have been assigned to and even turn the API off completely if they don’t want targetted adverts.</li>
</ul>
<p>The Topics API was first enabled in Chrome 115, released on July 17, 2023 as an <a href="https://developer.chrome.com/origintrials/">Origin Trial</a>. On September 20, 2023 the Origin trial ended and Topics is now available for the vast majority of Chrome users.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="526" class="figure__image" src="/images/cookie-banner/chrome-topics-settings.jpg" alt="The Topics settings for Google Chrome." />
</picture>
<figcaption class="figure__caption">The Google Chrome settings for the Topics API.</figcaption>
</figure>
<p>The current future of interest-based advertising on the web is uncertain. It is possible that these technologies will be replaced by new approaches in the future.</p>
<h2 id="network-isolation-and-http-cache-partitioning">Network isolation and HTTP Cache partitioning</h2>
<p>The next topic to discuss is another fairly modern development for browsers. Back in the early days of browsers there was a single browser cache that was used for every site you visited. This left site visitors open to a number of security and privacy attacks such as:</p>
<h3 id="detect-if-a-user-has-visited-a-specific-site">Detect if a user has visited a specific site</h3>
<p>An attacker can examine what is in the cache and infer what sites a user has visited from the cache contents. As certain resources may only be available on a specific set of sites.</p>
<h3 id="cross-site-search-attack-xs-search">Cross-site search attack (<a href="https://xsleaks.dev/docs/attacks/xs-search/">XS-Search</a>)</h3>
<p>An attacker can identify whether a particular search query yielded no results by examining the presence of a “no search results” image stored in the user’s browser cache.</p>
<h3 id="cross-site-tracking">Cross-site tracking</h3>
<p>The cache can be employed as a repository for persistent identifiers, enabling malicious actors to monitor user behaviour across multiple websites.</p>
<p>Thankfully, browser vendors have added security and privacy features to stop these attacks. The status of each major browser is listed below:</p>
<p><strong>Chrome</strong></p>
<p>Cache partitioning was first introduced in Chrome 86, which was released on October 5, 2020. Additional storage partitioning beyond the HTTP Cache was enabled from Chrome 115 which was released on October 4, 2022. See the Privacy Sandbox article <a href="https://developers.google.com/privacy-sandbox/3pcd/storage-partitioning">here</a> for more information.</p>
<p><strong>Firefox</strong></p>
<p>Cache partitioning, also known as network partitioning, was enabled by default for all users in Firefox 85, which was released on January 13, 2021. This feature was introduced as part of Firefox’s efforts to improve user privacy by making it more difficult for websites to track users across different websites. The browser also supports <a href="https://support.mozilla.org/en-US/kb/enhanced-tracking-protection-firefox-desktop">Enhanced Tracking Protection</a> which automatically protects a user’s privacy while they browse.</p>
<p><strong>Edge (v79+)</strong></p>
<p>Edge usually follows Chrome closely, and at the time of writing Chrome and Edge are aligned and stable in terms of Cache partitioning. More information on Microsoft’s plans for Edge and the Privacy-Preserving Ads API in 2024 can be found in <a href="https://blogs.windows.com/msedgedev/2024/03/05/new-privacy-preserving-ads-api/">this blog post published on the 5th March 2024</a>.</p>
<p><strong>Safari</strong></p>
<p>Apple has been way ahead of the curve with these features. The team actually talked about these features way back in <a href="https://bugs.webkit.org/show_bug.cgi?id=110269">February 2013</a>! For more information check out this post “<a href="https://andydavies.me/blog/2018/09/06/safari-caching-and-3rd-party-resources/">Safari, Caching and Third-Party Resources</a>“ by good friend <a href="https://twitter.com/andydavies">Andy Davies</a>.</p>
<h2 id="ios-intelligent-tracking-prevention-itp">iOS Intelligent Tracking Prevention (ITP)</h2>
<p>While we are on the subject of Apple and Safari, it’s probably a good time to mention their default tracking protection. Apple enabled <a href="https://webkit.org/blog/7675/intelligent-tracking-prevention/">Intelligent Tracking Prevention (ITP)</a> in Safari with the release of iOS 11 and macOS High Sierra on September 19, 2017.</p>
<p>ITP is used to stop sites from tracking a user across the internet. Safari automatically examines resources downloaded from a users’ journey around the internet.</p>
<p>Machine Learning is then used on the device to classify these resources to see if they could be used for tracking purposes. For more privacy, if a user hasn’t interacted with a website in the past 30 days the website data and cookies are immediately purged.</p>
<h3 id="itp-21">ITP 2.1</h3>
<p>In February 2019 Apple released <a href="https://webkit.org/blog/8613/intelligent-tracking-prevention-2-1/">ITP version 2.1</a> for iOS 12.2 and Safari 12.1 on macOS High Sierra and Mojave. This version brought major changes for sites wishing to track users. Third-party (tracking) cookies were blocked completely. Furthermore, any existing cookies that have had no user interaction for 30 days are then purged. More importantly for the GOV.UK cookie banner, any cookie set using the <code class="language-plaintext highlighter-rouge">document.cookie</code> API automatically expires after only <strong>7 days</strong>!</p>
<h2 id="implications-of-itp-21-for-govuk">Implications of ITP 2.1 for GOV.UK</h2>
<p>Although most users of iOS 12.2 and Safari 12.1 on macOS High Sierra and Mojave wouldn’t have realised ITP 2.1 had been installed and enabled on their devices. This was, in fact, a considerable change for websites that used this data for decision-making purposes.</p>
<p>GDS and the GOV.UK team in particular, were one of those impacted by ITP 2.1. Although I’ve heard many complaints over the years about this, GOV.UK uses Google Analytics (GA) for its analytics. The data captured is pretty limited. For example, whole sections of GA aren’t enabled internally, like “user demographics”, “behaviours”, and “interests” weren’t available for performance analysts to see. That’s not to say Google can’t see that data themselves!</p>
<p>But this is the hand we were dealt and a legacy of the age of GOV.UK. Back in 2012 when GOV.UK was first launched an Analytics product was needed to evaluate and prove that users were using GOV.UK and which parts were being used. User privacy wasn’t as important as it is today, and GA was likely the most feature rich (and easiest) to implement. In hindsight a self-hosted analytics solution should have been used like <a href="https://piwik.pro/">Piwik</a> (now <a href="https://matomo.org/">Motomo</a>). Although I can’t be sure if the versions at the time had all the features required by the GDS teams at the time (2007). See the <a href="#additional-context">additional context</a> from <a href="https://twitter.com/tomskitomski">Tom Loosemore</a> below for more information on the early decisions made by GDS about tracking.</p>
<h3 id="a-cookie-banner-every-7-days">A cookie banner every 7 days!</h3>
<p>With the release and rollout of ITP 2.1 on iOS and OSX this meant the annoying cookie banner at the top of GOV.UK would appear every 7 days. This is because the cookie used to store the information that the user had already seen it would be purged every week. So a user browsing GOV.UK first thing Monday morning on an iOS device, accepts the banner then exactly 1 week later they will need to do the same. This would be pretty frustrating for the user, but unfortunately, it’s completely outside any website’s control. So if you’re an iOS user and have ever wondered why you keep seeing the same cookie banner over and over again, this is the reason!</p>
<p><strong>A workaround to the 7 day problem</strong></p>
<p>Thanks to <a href="https://twitter.com/tunetheweb">Barry Pollard</a> for this info on a workaround. I quote:</p>
<blockquote>
<p>When you drop the cookie using client side JS, you make a JS API call a back-end which reads the cookie, and then sends it back as an HTTP set-cookie header. Boom - it’s no longer a JS cookie.</p>
</blockquote>
<blockquote>
<p>This is allowed AFAIK because Apple wants to block JS that isn’t on the same domain as the document (i.e. most likely tracking cookies). Their intention was never to block pure 1st party cookies as I understand it.</p>
</blockquote>
<h3 id="ios-mobile-bias">iOS Mobile Bias</h3>
<p>According to the monthly GOV.UK statistics I posted <a href="https://twitter.com/TheRealNooshu/status/1620739180119879680">back in February 2023</a>, Safari made up 31.48% of traffic in the month. And <a href="https://twitter.com/TheRealNooshu/status/1620739192270749696">56.21% of users were on a mobile</a>, with the <a href="https://twitter.com/TheRealNooshu/status/1620739195957575683">Apple iPhone being the most popular device</a> by an absolutely huge margin of 46.98%!</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="1498" class="figure__image" src="/images/cookie-banner/overall-stats-tweet.jpg" alt="Overall stats for January 2023 available here: https://twitter.com/TheRealNooshu/status/1620739180119879680" />
</picture>
<figcaption class="figure__caption">January 2023 GOV.UK stats: https://twitter.com/TheRealNooshu/status/1620739180119879680</figcaption>
</figure>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="2072" class="figure__image" src="/images/cookie-banner/mobile-stats-tweet.jpg" alt="Device type stats for January 2023 available here: https://twitter.com/TheRealNooshu/status/1620739192270749696" />
</picture>
<figcaption class="figure__caption">January 2023 Device type stats: https://twitter.com/TheRealNooshu/status/1620739192270749696</figcaption>
</figure>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="1362" class="figure__image" src="/images/cookie-banner/mobile-device-tweet.jpg" alt="Popular device type stats for January 2023: https://twitter.com/TheRealNooshu/status/1620739195957575683" />
</picture>
<figcaption class="figure__caption">January 2023 popular device type stats: https://twitter.com/TheRealNooshu/status/1620739195957575683</figcaption>
</figure>
<p>Unfortunately, with the ITP change in version 2.1 there’s no way of knowing how accurate these figures are. This is why I always posted a notice in the tweet thread saying:</p>
<blockquote>
<p>‡ Note: since December 2019 GOV.UK requires explicit opt-in for tracking which introduces bias. This is especially true for mobile devices, since the cookie banner takes up more screen estate and is more likely to be accepted.</p>
</blockquote>
<p>Which brings us back to the cookie banner design. This is what GOV.UK homepage looks like on Desktop:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1200" height="603" class="figure__image" src="/images/cookie-banner/govuk-desktop.jpg" alt="The cookie banner at the top of the GOV.UK homepage" />
</picture>
<figcaption class="figure__caption">The current GOV.UK cookie banner on desktop that pushes the main content down by approximately 45% of the height of the page.</figcaption>
</figure>
<p>This is what it looks like on Mobile:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="862" height="1754" class="figure__image" src="/images/cookie-banner/govuk-iphone.jpg" alt="The GOV.UK homepage with cookie banner on an iPhone 6" />
</picture>
<figcaption class="figure__caption">The current GOV.UK cookie banner on an iPhone 6 that pushes the main content down by approximately 65% of the height of the page. It is quite literally impossible to ignore on mobile, as you see so little of the pages actual content.</figcaption>
</figure>
<p>As you can see, the cookie banner is huge on any mobile device taking up around 65% of the screen. It’s also worth noting that a user is <strong>only</strong> seen by GA if they accept the cookie banner. As the cookie banner on a mobile is huge, a mobile user is more likely to “Accept” it so they can actually view the page content! This where the mobile bias comes into play with the GOV.UK analytics data.</p>
<p>Note: This only became an issue when the <a href="https://ico.org.uk/">Information Commissioner’s Office (ICO)</a> ruled that tracking all users by default wasn’t adhering to their <a href="https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/lawful-basis/consent/">explicit consent policy</a>.</p>
<h3 id="additional-context">Additional Context</h3>
<p>This is one of the reasons why I love blogging so much, it’s astonishing how much you can learn from the feedback other people give you.</p>
<p>And it’s also a great example of why you should keep some form of decision record in an organisation for when people leave and take all that precious knowledge with them! A huge thank you to <a href="https://twitter.com/tomskitomski">Tom Loosemore</a> for this additional context. Tom was Deputy Director, Government Digital Service, Cabinet Office, between January 2011 and October 2015.</p>
<p>I’ve directly quoted his comment he left me on LinkedIn related to this post:</p>
<blockquote>
<ol>
<li>
<p>Behind the scenes GDS tried very hard circa 2012 to convince the ICO that forcing cookie opt-in pop-ups would be counter productive to their laudable aims. Had they done proper user research as part of their response to <a href="https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guide-to-pecr/what-are-pecr/">PECR legislation</a> we might not be in this mess. Policy is delivery, redux.</p>
</li>
<li>
<p>The GOV.UK team blogged several times about balancing the need for user analytics with privacy, with a nicely intense debate in the comments. Sadly piwik was totally unsuitable as an alternative to GA. Enough well-meaning organisations had adopted it and then regretted it for us to know it was a non-starter. See this <a href="https://gds.blog.gov.uk/2012/01/12/cookies-on-the-beta/">blog post</a></p>
</li>
<li>
<p>Google would break its contract with HMG if it looked at or used the GA analytics data collected across *.gov.uk for anything other than protecting the GA service itself from attack. The options to stop Google employees (even GA support staff) accessing the *.gov.uk analytics data still seem strong enough to me. See this <a href="https://support.google.com/analytics/answer/1011397?hl=en&ref_topic=2919631#details-and-benefits&zippy=%2Cin-this-article">blog post</a>.</p>
</li>
</ol>
</blockquote>
<h2 id="the-public-suffix-list">The Public Suffix List</h2>
<p>When it comes to setting cookies across domains <code class="language-plaintext highlighter-rouge">gov.uk</code> and <code class="language-plaintext highlighter-rouge">service.gov.uk</code>, there’s an added complication. Both of these domains are on the <a href="https://publicsuffix.org/list/public_suffix_list.dat">Public Suffix List (PSL).</a></p>
<p>In simple terms, the PSL is a list of domain names that are considered to be effective top-level domains (eTLD). This has ramifications for setting cookies, that I will go into next.</p>
<p>If we wanted to stop the cookie banner appearing multiple times across a GOV.UK users journey, a cookie needs to be set to store the information that the banner has been seen and accepted. If we wanted to achieve this only using cookies, the PSL stops us from being able to do this.</p>
<p>As mentioned above <code class="language-plaintext highlighter-rouge">gov.uk</code> and <code class="language-plaintext highlighter-rouge">service.gov.uk</code> are in the PSL. So they are known as an effective top-level domain (eTLD). Browsers handle eTLD’s differently to other domains. It so happens that Network isolation and HTTP Cache partitioning are partitioned using the following URL structure <code class="language-plaintext highlighter-rouge">scheme://eTLD+1</code>. There’s an example below that, I hope, will explain it clearer:</p>
<p>For a GOV.UK user browsing from the <a href="https://www.gov.uk/">homepage</a> to the <a href="https://www.universal-credit.service.gov.uk/sign-in">Universal Credit Sign In</a> page the browser sees the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// ‘gov.uk’
eTLD = gov.uk
+1 = www
eTLD + 1 = https://www.gov.uk
// any other government service
eTLD = service.gov.uk
+1 = www.universal-credit
eTLD + 1 = https://www.universal-credit.service.gov.uk
OR
eTLD + 1 = https://www.check-mot.service.gov.uk/
OR
eTLD + 1 = https://www.payments.service.gov.uk/
OR
// I think you get the idea
</code></pre></div></div>
<p>The eTLD + 1’s are the keys used for cache and network partitioning. Due to this partitioning, any cookie set on <code class="language-plaintext highlighter-rouge">www.gov.uk</code> can’t be seen or shared on <code class="language-plaintext highlighter-rouge">www.universal-credit.service.gov.uk</code> or any other service domain. Thus, a user that accepted the cookie banner on the homepage will see it again in their user journey because this cookie can’t persist through to the service domain.</p>
<p>This is the reason the cookie banner “won’t go away”. Each service on a user’s journey, sees them as a brand-new user that needs to accept that service’s own cookie banner.</p>
<p>Unfortunately, as we mentioned earlier service domains follow the same GOV.UK branding, so many users won’t even realise they are browsing an entirely different website.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>So there we have it. A whole brain dump on the GOV.UK cookie banner, GOV.UK service design and surrounding browser technologies related to privacy and security. Hopefully, you found it interesting. Maybe it answered a few questions you didn’t even want to know 😂 As always if you have any questions or feedback, please do <a href="https://twitter.com/TheRealNooshu/">get in touch</a>.</p>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>12/12/23: Initial blog post published. Happy Birthday to me!</li>
<li>12/12/23: Thanks (again) to <a href="https://twitter.com/tunetheweb">Barry Pollard</a> for the numerous fixes to the <a href="#cross-site-tracking">Cross Site Tracking section of the post</a> and a workaround for the 7 day time limit on Safari!</li>
<li>13/12/23: A huge thanks to <a href="https://twitter.com/tomskitomski">Tom Loosemore</a> for his incredibly informative LinkedIn post on the ICO and GA in the early years of GOV.UK. His update can be <a href="#additional-context">seen here</a>.</li>
<li>13/12/23: Thanks to <a href="https://twitter.com/joelanman">Joe Lanman</a> for pointing out it’s Privacy and Electronic Communications Regulations (PECR) not General Data Protection Regulation (GDPR) that is important for cookie banners.</li>
<li>13/12/23: Thanks to <a href="https://toot.cafe/@slightlyoff">Alex Russell</a>, for getting back to me about the status of Cache partitioning in Edge.</li>
<li>05/03/24: Thanks to <a href="https://twitter.com/rowan_m">Rowan Merewood</a>, for the new information on what the Edge team has planned for 2024 in their <a href="https://blogs.windows.com/msedgedev/2024/03/05/new-privacy-preserving-ads-api/">latest blog post</a>.</li>
</ul>Matt HobbsThis is a blog post about the GOV.UK cookie banner and the reason it "won't go away" when navigating across UK government services. I also touch on security and privacy features available in modern browsers.Remove Subresource Integrity (SRI) from the GOV.UK assets domain2023-10-23T09:00:00+00:002023-10-23T09:00:00+00:00https://nooshu.com/blog/2023/10/23/remove-sri-on-govuk-assets-domain<hr />
<p><strong>Note</strong>: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to remove Subresource Integrity (SRI) from the assets served from the <code class="language-plaintext highlighter-rouge">assets</code> domain of GOV.UK, thus enabling better performance for the HTTP/2 protocol. It was originally posted on the <a href="https://github.com/alphagov/govuk-rfcs">GOV.UK RFCs GitHub repo</a>. It can be <a href="https://raw.githubusercontent.com/alphagov/govuk-rfcs/7e8909e095af59003179e055629522b1d38a48e1/rfc-112-remove-sri-on-assets.md">viewed here</a>. I’m posting it to my blog so as to own my own work and hopefully give it more visibility. I wrote a blog post about the changes but the content was deemed too technical for the “Technology in Government blog”, so I pushed for it to be published as a case study instead, which can be <a href="https://www.gov.uk/government/case-studies/how-gds-improved-govuks-frontend-performance-with-http2">seen here</a>. I also wrote about this issue on my personal blog the post is called <a href="/blog/2019/12/17/http2-and-sri-dont-always-get-on/">HTTP/2 and Subresource Integrity don’t always get on</a>.</p>
<hr />
<h2 id="summary">Summary</h2>
<p>Due to the security requirements of SRI, <code class="language-plaintext highlighter-rouge">crossorigin="anonymous"</code> is required for these assets. This forces browsers to open a separate TCP connection to download this resource. Since TCP connections are expensive to open and maintain, this practice has a negative effect on frontend performance.</p>
<h2 id="problem">Problem</h2>
<p>Running <a href="https://www.webpagetest.org/">Web Page Test</a> (WPT) against a number of pages across GOV.UK reveals that the browser is having to open a greater number of TCP connections due to the use or SRI, and more specifically the <code class="language-plaintext highlighter-rouge">crossorigin="anonymous"</code> attribute attached to the CSS and JavaScript references. This attribute is mandatory for <a href="https://shubhamjain.co/til/subresource-integrity-crossorigin/">security against cross origin policy violations</a>. The use of the <code class="language-plaintext highlighter-rouge">crossorigin="anonymous"</code> forces the browser to open a brand new TCP connection to ensure that no user credentials are shared across the connection. This can be seen in the images below:</p>
<h3 id="homepage">Homepage</h3>
<p>In this example 11 TCP connections are being opened when the browser default for <a href="https://docs.pushtechnology.com/cloud/latest/manual/html/designguide/solution/support/connection_limitations.html">HTTP/1.1 in many browsers is 6</a>. Since opening TCP <a href="https://hpbn.co/building-blocks-of-tcp/#three-way-handshake">connections is expensive</a>, this will be having a negative effect on performance.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1870" height="832" class="figure__image" src="/images/rfc-112/homepage-connection-view.png" alt="The WebPageTest connection view before SRI was removed." />
</picture>
</figure>
<p>Full test can no longer be seen due to WebPageTest being sold to CatchPoint.</p>
<h3 id="past-prime-ministers-page">Past Prime Ministers page</h3>
<p>The same issue can be seen on the Past Prime ministers page, where 13 TCP connections are opened instead of the default 6 a browser usually does. As can be seen in the bandwidth graph the connection isn’t being fully utilised, this is probably due to the TCP slow start is an algorithm that each TCP connection uses.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1866" height="970" class="figure__image" src="/images/rfc-112/past-pm-connection-view.png" alt="The WebPageTest connection view on the past Prime Ministers page is even more complex with a total of 14 TCP connections." />
</picture>
</figure>
<p>Full test can no longer be seen due to WebPageTest being sold to CatchPoint.</p>
<h3 id="html-size">HTML size</h3>
<p>A very minor point, but adding the <code class="language-plaintext highlighter-rouge">crossorigin="anonymous" integrity="sha256-wLi6ixZSqsrSmNdPJHUiYBh/U4tQxAwkhPfzM8vDzys="</code> to the 12 resource requests across a page could be adding an extra 1KB to the HTML size (although GZipping will minimise this).</p>
<h3 id="http2">HTTP/2</h3>
<p>Will HTTP/2 solve this issue? Since HTTP/2 multiplexes assets over different streams on a single TCP connection. Unfortunately not. Requests without credentials use a separate connection as mentioned in the article <a href="https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/#requests-without-credentials-use-a-separate-connection">here</a>. So even when we eventually enable HTTP/2 we will have the same issue where an anonymous connection will be opened.</p>
<h2 id="proposal">Proposal</h2>
<p>I’m proposing we disable SRI for all assets from the <code class="language-plaintext highlighter-rouge">https://assets.publishing.service.gov.uk</code> domain. Since this is a domain under our control it is highly unlikely that a MITM attack will occur. Assuming an attacker gets that far they would most likely be able bring the whole site down anyway due to the level of access they would need. Removing SRI will then allow us to remove the mandatory <code class="language-plaintext highlighter-rouge">crossorigin="anonymous"</code>, which will then allow the browser to open and reuse connections as they are intended (and hit the maximum of 6 connections).</p>
<p>For assets that do come from a third-party domain, they should still have SRI enabled for security reasons.</p>Matt HobbsThis is an RFC I wrote to persuade the GOV.UK Senior Technology team to remove Subresource Integrity (SRI) from GOV.UK's assets being served from the assets domain and allow for greater HTTP/2 performance.Enabling Brotli compression on GOV.UK2023-09-28T09:30:00+00:002023-09-28T09:30:00+00:00https://nooshu.com/blog/2023/09/28/enabling-brotli-compression-on-govuk<hr />
<p><strong>Note</strong>: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable Brotli compression on GOV.UK. It was originally posted on the <a href="https://github.com/alphagov/govuk-rfcs">GOV.UK RFCs GitHub repo</a>. It can be <a href="https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-138-enable-brotli-compression.md">viewed here</a>. I’m posting it to my blog so as to own my own work and hopefully give it more visibility. A blog post was drafted about this change but unfortunatly never published, as it was considered too technical for a GDS blog post.</p>
<hr />
<h2 id="summary">Summary</h2>
<p>Brotli compression is a new compression algorithm that is available in modern browsers that can offer 10-20% better compression over gzip compression. By enabling this option on the CDN users with browsers that support it will receive smaller static files over the wire (HTML, CSS, JavaScript), and those that don’t will continue to receive assets that are gzipped.</p>
<h2 id="problem">Problem</h2>
<p>On GOV.UK we should always be looking to reduce the number of bytes a user has to download to view our pages. This is both good for web performance and data consumption on user devices. Any browser that supports WOFF2 fonts will also support Brotli compression (since this is the compression used in the fonts). According to the latest stats from <a href="https://caniuse.com/brotli">caniuse.com</a>, the Brotli <code class="language-plaintext highlighter-rouge">Accept-Encoding</code>/<code class="language-plaintext highlighter-rouge">Content-Encoding</code> functionality is supported by 95% of user browsers globally, and I expect this figure will actually be bigger if we only consider UK based users:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1362" height="466" class="figure__image" src="/images/rfc-138/brotli-usage-caniuse.png" alt="Image of browser support for the brotli compression header." />
</picture>
</figure>
<p>This compression has been enabled on both integration and staging. We saw the following savings in terms of total page bytes for HTML, CSS, and JavaScript across the frontend applications of GOV.UK:</p>
<table>
<thead>
<tr>
<th>Page</th>
<th>Application</th>
<th>Before (b)</th>
<th>After (b)</th>
<th>Diff (%)</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>frontend</td>
<td>259,371</td>
<td>248,653</td>
<td>-4.13</td>
</tr>
<tr>
<td>/coronavirus</td>
<td>info-frontend</td>
<td>288,650</td>
<td>272,687</td>
<td>-5.53</td>
</tr>
<tr>
<td>/browse/driving/driving-licences</td>
<td>collections</td>
<td>278,385</td>
<td>265,832</td>
<td>-4.51</td>
</tr>
<tr>
<td>/contact</td>
<td>feedback</td>
<td>242,047</td>
<td>232,124</td>
<td>-4.10</td>
</tr>
<tr>
<td>/search/all</td>
<td>finder-frontend</td>
<td>313,400</td>
<td>295,979</td>
<td>-5.56</td>
</tr>
<tr>
<td>/bank-holidays</td>
<td>frontend</td>
<td>263,686</td>
<td>251,555</td>
<td>-4.60</td>
</tr>
<tr>
<td>/national-minimum-wage-rates</td>
<td>government-frontend</td>
<td>266,699</td>
<td>254,073</td>
<td>-4.73</td>
</tr>
<tr>
<td>/info/coronavirus/business-support</td>
<td>info-frontend</td>
<td>233,133</td>
<td>226,382</td>
<td>-2.90</td>
</tr>
<tr>
<td>/licence-finder/sectors</td>
<td>licence-finder</td>
<td>242,555</td>
<td>234,969</td>
<td>-3.13</td>
</tr>
<tr>
<td>/guidance/immigration-rules</td>
<td>manuals-frontend</td>
<td>250,431</td>
<td>240,763</td>
<td>-3.86</td>
</tr>
<tr>
<td>/service-manual/service-standard</td>
<td>service-manual-frontend</td>
<td>254,529</td>
<td>245,881</td>
<td>-3.40</td>
</tr>
<tr>
<td>/additional-commodity-code/y</td>
<td>frontend</td>
<td>250,664</td>
<td>240,974</td>
<td>-3.87</td>
</tr>
<tr>
<td>/government/people/theresa-may</td>
<td>whitehall</td>
<td>277,888</td>
<td>265,642</td>
<td>-4.41</td>
</tr>
</tbody>
</table>
<p>These savings for the pages tested converted into the following performance improvements. All tested on a simulated Moto G4 mobile on a 3G connection.</p>
<h3 id="homepage">Homepage</h3>
<p>The visual progress of the homepage has improved by approximately 110 ms as can be seen in the visual progress graph:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="897" height="398" class="figure__image" src="/images/rfc-138/visual-progress-homepage.png" alt="Visual progress of the homepage using both compression methods." />
</picture>
</figure>
<p>And we see the a reduction in bytes for all the expected assets:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="895" height="594" class="figure__image" src="/images/rfc-138/total-bytes-homepage.png" alt="bytes for assets compared on the GOV.UK homepage" />
</picture>
</figure>
<h3 id="coronavirus-page">Coronavirus page</h3>
<p>The visual progress graph for this page starts off worse by 400ms, but quickly catches up and the viewport for Brotli completes rendering 200ms before gzip:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="899" height="399" class="figure__image" src="/images/rfc-138/visual-progress-covid.png" alt="visual progress graph for the covid page." />
</picture>
</figure>
<p>And we see the a reduction in bytes for all the expected assets:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="" height="" class="figure__image" src="/images/rfc-138/total-bytes-coronavirus.png" alt="bytes for assets compared on the coronavirus page" />
</picture>
</figure>
<p>Showing a 5.53% reduction.</p>
<h3 id="bank-holidays">Bank holidays</h3>
<p>The visual progress of the bank holidays has improved by approximately 200ms as can be seen in the visual progress graph:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="898" height="398" class="figure__image" src="/images/rfc-138/visual-progress-bank-holidays.png" alt="Visual progress of the bank holidays using both compression methods." />
</picture>
</figure>
<p>And we see the a reduction in bytes for all the expected assets:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="897" height="597" class="figure__image" src="/images/rfc-138/total-bytes-bank-holidays.png" alt="bytes for assets compared on the bank holidays page." />
</picture>
</figure>
<p>Showing a 4.60% reduction.</p>
<h3 id="search-page">Search page</h3>
<p>The visual progress of the search page has improved by approximately 400ms initially, 200ms at the end. As can be seen in the visual progress graph:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="899" height="399" class="figure__image" src="/images/rfc-138/visual-progress-search.png" alt="Visual progress of the search page using both compression methods." />
</picture>
</figure>
<p>And we see the a reduction in bytes for all the expected assets:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="897" height="598" class="figure__image" src="/images/rfc-138/total-bytes-search.png" alt="bytes for assets compared for the search page" />
</picture>
</figure>
<p>Showing a 5.56% reduction.</p>
<h3 id="summary-1">Summary</h3>
<p>In all the pages tested across the frontend apps (17 in total) we see between 3-6% reduction in the number of bytes a browser is having to download over the network. This saving is being converted to improvements in the visual metrics a users is seeing in the browser.</p>
<h2 id="proposal">Proposal</h2>
<p>The proposal is to enable Brotli compression at the edge using the <a href="https://www.fastly.com/release-notes/q3-2020#brotli">Fastly Brotli Compression LA</a>. Browsers that support Brotli will receive these versions of the files. Any browsers that don’t will receive the gzip version of the file. This can therefore be applied using the progressive enhancement methodology. In doing so we MUST update the configuration VCL so as to automate this process. During testing it was simply enabled via the Fastly user interface.</p>
<p>We SHOULD also purge the static cache once completed as it was found during testing that certain Fastly PoP’s continued to serve the uncompressed version of the file since there was a mismatch between <code class="language-plaintext highlighter-rouge">Accept-Encoding</code> header from the browser and <code class="language-plaintext highlighter-rouge">Content-Encoding</code> version available on the CDN. Once the cache was purged this issue was rectified. However, this issue would rectify itself over time even without the manual purging, since Rails inserts an MD5 fingerprint into the filename of each file. Once a file is changed it will have a new URL, thus invalidating the old cache.</p>Matt HobbsThis is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable Brotli compression on GOV.UK. It was accepted and merged into the repo, and enabled in March 2021.Enabling HTTP/2 on GOV.UK2023-09-28T09:00:00+00:002023-09-28T09:00:00+00:00https://nooshu.com/blog/2023/09/28/enabling-http2-on-govuk<hr />
<p><strong>Note</strong>: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/2 on GOV.UK. It was originally posted on the <a href="https://github.com/alphagov/govuk-rfcs">GOV.UK RFCs GitHub repo</a>. It can be <a href="https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-115-enabling-http2-on-govuk.md">viewed here</a>. I’m posting it to my blog so as to own my own work and hopefully give it more visibility. The Technology in Government blog post I wrote on the work can be <a href="https://technology.blog.gov.uk/2020/07/02/speeding-up-gov-uk-with-http-2/">seen here</a>.</p>
<hr />
<h2 id="summary">Summary</h2>
<p>Back in November 2018 we trialed the use of HTTP/2 on GOV.UK. According to quite a few sources, enabling HTTP/2 should improve web performance for users by introducing technology like multiplexed streams, HPACK header compression and stream prioritisation. Unfortunately it turned out that from our synthetic web performance testing it actually slowed the site down in many instances.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1460" height="434" class="figure__image" src="/images/rfc-115/cold-cache-summary.png" alt="Results from testing HTTP/1.1 vs HTTP/2." />
</picture>
</figure>
<p>We tested 5 different page types, on multiple devices and connection speeds and examined the following performance metrics to come up with a result:</p>
<ul>
<li>First visual change</li>
<li>Visually complete 95%</li>
<li>Last visual change</li>
<li>Speed index</li>
<li>Load time (fully loaded)</li>
</ul>
<p>And for Lighthouse reports these metrics were examined:</p>
<ul>
<li>First Contentful Paint</li>
<li>First Meaningful Paint</li>
<li>Speed Index</li>
<li>First CPU Idle</li>
<li>Time to Interactive</li>
</ul>
<p>The RFC below discusses the problems with our current setup and suggests possible solutions.</p>
<h2 id="problems">Problems</h2>
<h3 id="1---sub-resource-integrity-sri">1 - Sub Resource Integrity (SRI)</h3>
<p>On GOV.UK we are using Subresource Integrity for all our CSS and JavaScript assets coming from the assets domain. The <a href="https://www.w3.org/TR/SRI/#cross-origin-data-leakage">SRI specification</a> requires that the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute is set to <code class="language-plaintext highlighter-rouge">anonymous</code> to be used with SRI resources for security reasons. This is because of data leakage from credentialed TCP connections. This requirement is forcing the browser to open a second TCP connection in ‘anonymous mode’ so it can download the CSS / JS from the assets domain. In doing so this is adding 100’s of milliseconds of delay to the page rendering timeline. This occurs even when using the <a href="https://www.w3.org/TR/resource-hints/#fetching-the-resource-hint-link"><code class="language-plaintext highlighter-rouge">preconnect</code></a> resource hint, a browser feature intended to help fix this issue.</p>
<p>This performance issue is occurring in both HTTP/1.1 and HTTP/2 as seen in the WebPageTest connection view waterfalls below:</p>
<h4 id="http11">HTTP/1.1</h4>
<figure class="figure">
<picture>
<img loading="lazy" width="1000" height="367" class="figure__image" src="/images/rfc-115/connection-view-annotated.png" alt="The connection view can tell you a lot about how your connections are being utilised. Focus on each one in turn and see how much of the row is empty. This will show you the wasted time on each connection." />
</picture>
</figure>
<p>Here we can see 13 TCP connections being opened (6 ‘credentialed’, 6 ‘anonymous’, 1 third-party to Google Analytics). If we weren’t using SRI, we could reduce this requirement down to 8 (6 ‘credentialed’, 1 ‘anonymous’ for fonts, 1 third-party to Google Analytics). This is the first step in improving web performance for GOV.UK users on HTTP/1.1.</p>
<h4 id="http2">HTTP/2</h4>
<p>Below you can see the delay in the waterfall while the 2nd <code class="language-plaintext highlighter-rouge">anonymous</code> TCP connection is established:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="930" height="299" class="figure__image" src="/images/rfc-115/h2-dns-annotated.png" alt="The delay seen in the HTTP/2 waterfall chart" />
</picture>
</figure>
<p>And this is what it could look like if we were to remove SRI:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1000" height="299" class="figure__image" src="/images/rfc-115/the-impact-annotated.png" alt="The impact removing SRI has on the waterfall chart" />
</picture>
</figure>
<p>In the example test above on a Nexus 5 device under 3G connection speeds, we could bring the request of the CSS & JS files forwards by ~750 ms. This should speed up the whole waterfall and turn the results from the summary list above from red to green in favour of HTTP/2.</p>
<p>This is achieved through the use of HTTP/2 connection coalescing, which can be seen in action on GOV.UK from our trial below:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1000" height="197" class="figure__image" src="/images/rfc-115/connection-view.png" alt="Example of an optimised connection view waterfall." />
</picture>
</figure>
<p>The coalesced connection is under-utilised if ‘anonymous mode’ is used on our static assets. There’s also an impact from the fact that <a href="https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start">TCP Slow Start</a> is in action on the delayed <code class="language-plaintext highlighter-rouge">anonymous</code> connection, meaning assets will download slower than they could via the single coalesced connection. So we are in the following situation: the connection used to download the HTML isn’t utilised fully (by this point it will already be up to speed), and the <code class="language-plaintext highlighter-rouge">anonymous</code> connection downloading the critical CSS has been delayed, so it isn’t up to speed yet. For best performance we should be utilising the connection that has already been established via the HTML download, and use it for other critical page assets (CSS/JS).</p>
<h3 id="2---assets-served-with-access-control-allow-origin-">2 - Assets served with <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code></h3>
<p>My initial thinking was that we could quickly switch the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute from <code class="language-plaintext highlighter-rouge">anonymous</code> to <code class="language-plaintext highlighter-rouge">use-credentials</code> for our static assets (CSS/JS). Unfortunately on examining the <a href="https://fetch.spec.whatwg.org/">Fetch specification</a> there’s <a href="https://fetch.spec.whatwg.org/#cors-protocol-and-credentials">information in the table (5th row down)</a> that states:</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">Access-Control-Expose-Headers</code>, <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Methods</code>, and <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Headers</code> response headers can only use <code class="language-plaintext highlighter-rouge">*</code> as value when request’s credentials mode is not “include”.</p>
</blockquote>
<p>Basically, the use of the wildcard (<code class="language-plaintext highlighter-rouge">*</code>) isn’t allowed on a credentialed connection (<code class="language-plaintext highlighter-rouge">use-credentials</code>). And if it is used, the browser will block any requests and raise an error message the that looks like this:</p>
<blockquote>
<p>Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘https://assets.example.com/script.js’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).</p>
</blockquote>
<p>We are currently <a href="https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38">serving all our assets</a> from the <code class="language-plaintext highlighter-rouge">assets</code> domain with the <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code> header, which is blocking us from making this change. The reason why we are serving assets like this is because we are using SRI and they need the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute for it to work.</p>
<p>The Heroku applications (e.g. https://government-frontend.herokuapp.com) that are created are a legitimate reason for using this header when <code class="language-plaintext highlighter-rouge">crossorigin</code> is present. If we were to remove the <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code> header then the JS and fonts (due to their <a href="https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements">unique CORS requirements</a>) on the Heroku previews would break. According to the <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">documentation</a>, there are only 3 valid values for this header:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">*</code></li>
<li><code class="language-plaintext highlighter-rouge"><origin></code></li>
<li><code class="language-plaintext highlighter-rouge">null</code></li>
</ul>
<p>The <code class="language-plaintext highlighter-rouge"><origin></code> value is very important. As this is where we would ideally like to specify: <code class="language-plaintext highlighter-rouge">*.gov.uk; *.herokuapp.com</code>. Unfortunately this header only accepts a <strong>single</strong> origin, and it <strong>doesn’t</strong> recognise wildcard values. So we are in a situation where we need to use <code class="language-plaintext highlighter-rouge">*</code> (for SRI) and a specific origin value (e.g <code class="language-plaintext highlighter-rouge">https://government-frontend.herokuapp.com</code> for every unique Heroku app URL, including individual PR’s) at the same time. There’s the possibility that this can be done by adjusting the <a href="https://www.fastly.com/blog/caching-cors">VCL config on the CDN</a>, but that then opens us out to more potential complications as listed in @kevindew’s comment <a href="https://github.com/alphagov/govuk-rfcs/pull/115#discussion_r366510706">here</a>. So this could complicate both the CDN configuration and local development if not properly investigated.</p>
<h3 id="3---the-assets-domain">3 - The assets domain</h3>
<p>Domain sharding for static assets is an anti-pattern under HTTP/2, and our current HTTP/1.1 setup isn’t optimal for performance either. If our static assets weren’t being served including SRI we would be able to remove the <code class="language-plaintext highlighter-rouge">crossorigin</code> and <code class="language-plaintext highlighter-rouge">integrity</code> attributes from the <code class="language-plaintext highlighter-rouge"><script></code> and <code class="language-plaintext highlighter-rouge"><link></code> tags. This in turn would allow the browser to correctly use <a href="https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/">HTTP/2 connection coalescing</a>, which would minimise the impact the assets domain is having on HTTP/2 performance.</p>
<p>Some browser implementations of HTTP/2 connection coalescing are <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1011685">notoriously flaky</a>, so this performance benefit will only be available in some browsers. Users on older versions of Safari (before 12), IE, and Edge 18 and below, all don’t support coalescing, so won’t benefit from this optimisation. But as we’ve seen above, the domain sharding isn’t offering any benefits anyway. So why not remove the need for connection coalescing completely for our static assets and serve them from the origin. All browsers that support HTTP/2 will then receive the same benefits and experience of the browser using a single multiplexed TCP connection. <a href="https://caniuse.com/#search=http2">95.7% of the UK population</a> use a browser that supports HTTP/2, so most of our users will benefit from this change.</p>
<p>Once completed our connection graph will look similar to this:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1000" height="229" class="figure__image" src="/images/rfc-115/clean-connections.png" alt="Example of an optimised connection graph" />
</picture>
</figure>
<p>The bulk of the page assets will be downloaded on the initial connection to the origin (www.gov.uk), with a secondary <code class="language-plaintext highlighter-rouge">anonymous</code> connection only being opened for the fonts. That’s twelve connections under our current HTTP/1.1 implementation, down to just two for HTTP/2 in many cases. So in order to fix our HTTP/1.1 and HTTP/2 performance issues we should serve static assets (CSS, JavaScript, fonts, images) all from the origin (www.gov.uk).</p>
<h3 id="summary-1">Summary</h3>
<p>In order to enable HTTP/2 on the GOV.UK domain (and for it to perform well) we need to all problems listed above:</p>
<ul>
<li>SRI requires the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute to be set for it to work.</li>
<li>The use of <code class="language-plaintext highlighter-rouge">crossorigin='anonymous'</code> breaks HTTP/1.1 connection reuse and HTTP/2 connection coalescing.</li>
<li>Switching to use <code class="language-plaintext highlighter-rouge">crossorigin='use-credentials'</code> isn’t possible because we serve our assets with the <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code> header so that the assets leveraging SRI can be used on other domains (e.g. Heroku).</li>
<li>Even if we serve the static assets from the origin, we will still need the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute because we will still be loading them with SRI enabled.</li>
</ul>
<p>Therefore my recommendation is to remove the <code class="language-plaintext highlighter-rouge">integrity</code> and <code class="language-plaintext highlighter-rouge">crossorigin</code> attributes from our static resources (CSS, JS) and work towards serving these assets from the origin domain so we are no longer reliant on an individual browsers implementation of HTTP/2 connection coalescing. NOTE: it is possible to do these streams of work separately.</p>
<p>We should only serve our fonts with the <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code> header (this is a requirement when using web fonts). All other assets can have this header removed (as we will have removed the <code class="language-plaintext highlighter-rouge">crossorigin</code> attribute). We only need to be serving the following font files:</p>
<ul>
<li>EOT - For Internet Explorer only (6-11)</li>
<li>WOFF - For all modern versions of “evergreen” browsers and IE9-11.</li>
<li>WOFF2 - All modern browsers since ~2016. it’s usage trumps the use of WOFF.</li>
</ul>
<p>EOT files are no longer going to be served to Internet Explorer once GOV.UK has migrated fully to the Design System. So EOT can be removed in the future. There are no plans that I am aware of to create another webfont format, so we won’t be needing to add any more for the foreseeable future.</p>
<p>SRI Note: In serving the static assets from the origin domain, SRI no longer serves any purpose, as it’s a security measure intended to be used on third party resources. As all of our assets will be first party, it really isn’t required and serves only to complicate our setup and impact on performance.</p>
<p>Serving static assets from the origin also clears a path for future web performance enhancements like <a href="https://www.fastly.com/blog/why-fastly-loves-quic-http3">HTTP/3, QUIC (Quick UDP Internet Connection)</a> and 0-RTT. <a href="https://caniuse.com/#feat=http3">HTTP/3</a> has no support at the moment as it currently sits behind browser flags. But at some point in the future it will be supported and the asset domain will need to be removed for static assets anyway, to keep up with the latest protocol developments.</p>
<p>NOTE: As mentioned by @david-ncsc in <a href="https://github.com/alphagov/govuk-rfcs/pull/115#issuecomment-567907397">his comment</a>, there’s probably a security benefit to keeping the uploaded files on a separate origin as defence against uploaded malicious files. We should therefore only serve the static assets (CSS, JavaScript, fonts) from the origin. All other assets can stay the same.</p>
<h2 id="proposal">Proposal</h2>
<h3 id="must">MUST</h3>
<ul>
<li>Only serve our font assets with <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Origin: *</code> header (WOFF2, WOFF, EOT). - Medium (work required to modify NGINX config <a href="https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38">here</a> and <a href="https://github.com/alphagov/govuk-puppet/blob/master/modules/govuk/templates/asset_pipeline_extra_nginx_conf.erb">here</a>)</li>
<li>Remove the <code class="language-plaintext highlighter-rouge">integrity</code> and <code class="language-plaintext highlighter-rouge">crossorigin</code> attributes from our static resources (CSS, JS) - Easy (PR’s quickly raised for all apps CSS/JS).</li>
<li>Serve static assets from the origin rather than the assets domain - Medium/Hard (spike already investigated by @kevindew and commented on <a href="https://github.com/alphagov/govuk-rfcs/pull/115#issuecomment-573008991">here</a>).</li>
</ul>
<h3 id="should">SHOULD</h3>
<ul>
<li>Review the use of <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Methods</code> and <code class="language-plaintext highlighter-rouge">Access-Control-Allow-Headers</code> headers as from looking at the CORS documentation they aren’t actually needed. - Easy (Code resides <a href="https://github.com/alphagov/govuk-puppet/blob/962ea899e9c6778fe91e80074346912bd4314b10/modules/router/templates/assets_origin.conf.erb#L36-L38">here</a>).</li>
</ul>
<h3 id="consequences">Consequences</h3>
<ul>
<li>Browsers that support HTTP/2 will be allowed to use it to its maximum potential. Browsers that don’t will still use HTTP/1.1, but will now only require 6 TCP connections rather than 12.</li>
<li>Both H1 and H2 users should receive improved performance due to improved TCP connection efficiencies.</li>
<li>Under HTTP/2 we should see only 2 TCP connections on the WebPageTest connection graph, with the vast majority of assets loading via the same connection as the HTML.</li>
</ul>
<h3 id="how-to-enable-http2">How to enable HTTP/2</h3>
<p>Once we have made the changes how do we enable HTTP/2, and what changes need to be made to the applications?</p>
<ul>
<li>I simply contact Fastly support and ask them to enable it on GOV.UK. This will enable it on production, integration, and the assets domains.</li>
<li>No changes to any infrastructure or apps need to be made, as this is a transport layer change on the CDN.</li>
</ul>Matt HobbsThis is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/2 on GOV.UK. It was accepted and merged into the repo, and enabled in June 2020.Enabling HTTP/3 on GOV.UK2023-09-13T12:09:00+00:002023-09-13T12:09:00+00:00https://nooshu.com/blog/2023/09/13/enabling-http3-on-govuk<hr />
<p><strong>Note</strong>: This is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/3 on GOV.UK. It was originally posted on the <a href="https://github.com/alphagov/govuk-rfcs">GOV.UK RFCs GitHub repo</a>. It can be <a href="https://github.com/alphagov/govuk-rfcs/blob/4d28064a477ab225864651f7c912c5aeae6c2fc1/rfc-139-enable-http3.md">viewed here</a>. I’m posting it to my blog so as to own my own work and hopefully give it more visibility.</p>
<hr />
<h2 id="summary">Summary</h2>
<p>HTTP/3 is the latest version of the HTTP protocol that is currently in use in production by Facebook and Google, as well as many others. It offers web performance improvements over HTTP/2, specifically when connections with high packet loss are being used. Users on unstable mobile connections and rural areas should see the most improvement with this new protocol adoption.</p>
<h2 id="problem">Problem</h2>
<p>According to the latest data (October 2020) by <a href="https://www.measurementlab.net/">MLab</a>, the UK has a median download speed of 20 MBit/s and 7.2 MBit/s upload speed. However, exporting this dashboard data to a spreadsheet paints a very different picture.</p>
<p>The data shows that there are parts of the UK that have connection speeds less than 0.3 MBit/s for both download, and upload. This is roughly equivalent to a 2G connection. It’s worth noting that as these are median values, many users in these areas will be seeing much lower connection speeds than this in reality. These areas have a connection speed that is 98.5% slower than the median for the whole country.</p>
<p>Users in these areas still have a need to access government services and the information that GOV.UK offers, but under these conditions it won’t be a very pleasant experience. These are the users that will benefit from a transition to HTTP/3.</p>
<h2 id="brief-history-of-http">Brief history of HTTP</h2>
<p>Each iteration of the HTTP protocol has set out to improve the web performance of the previous version of the protocol.</p>
<h3 id="http10">HTTP/1.0</h3>
<p>The first version of the HTTP protocol came with a number of web performance issues, a primary issue being that only a single asset could be downloaded on a Transmission Control Protocol (TCP) connection before the connection was then closed. There was no way for these connections to persist for multiple asset downloads. This resulted in a lot of wasted negotiation time to establish separate TCP connections for each asset.</p>
<h3 id="http11">HTTP/1.1</h3>
<p>HTTP/1.1 fixed the persistent connections issue, allowing multiple assets to be downloaded over a single connection once established. Unfortunately this didn’t fully solve the web performance issues seen with the HTTP protocol (stemming from the TCP layer).</p>
<p>Under HTTP/1.1 the browser must download 100% of an asset before it can move onto the next asset. Meaning it can only download a single asset at a time per connection, the other assets are blocked. This is called Head-of-Line Blocking (HOL Blocking). Browser vendors quickly tried to solve this issue by allowing a browser to open up to 6 TCP connections per domain, so 6 assets can be downloaded in parallel.</p>
<p>Unfortunately a number of issues still exist with this method:</p>
<ol>
<li>Most sites have many more than 6 assets per page, so HOL blocking is still occurring in almost every page load under HTTP/1.1.</li>
<li>Each TCP connection is independent and have no knowledge of each other. All 6 are fighting for limited bandwidth which in turn can saturate the network.</li>
<li>Many websites resorted to “domain sharding” (hosting assets across multiple domains), to allow a browser to open up more that 6 TCP connections. This in turn exacerbated the problem mentioned in point 2.</li>
</ol>
<p>Another solution needed to be found.</p>
<h3 id="http2">HTTP/2</h3>
<p>HTTP/2 is a major iteration on the HTTP protocol. It started life as the <a href="https://en.wikipedia.org/wiki/SPDY">SPDY protocol</a> created by Google, which was eventually used as the foundation for HTTP/2. One major shift in this version is the ability to multiplex stream assets over a single TCP connection.</p>
<p>Each TCP packet now includes a set of “frames” that contains additional meta information about each stream. Each asset is downloaded over a separate stream and then reconstructed by the browser. This allows multiple assets to be downloaded at the same time over a single TCP connection, removing the HOL blocking at the application (HTTP) layer. An example of this setup can be seen in the diagram below:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1948" height="760" class="figure__image" src="/images/rfc-139/5_H2_2files_multiplexed.png" alt="An example of HTTP/2 frames within TCP packets." />
</picture>
</figure>
<p>Source: <a href="https://calendar.perfplanet.com/2020/head-of-line-blocking-in-quic-and-http-3-the-details/">“Head-of-Line Blocking in QUIC and HTTP/3: The Details”</a> by <a href="https://twitter.com/programmingart">Robin Marx</a>.</p>
<p>Unfortunately this didn’t solve all the HOL blocking issues. HTTP/2 is built upon TCP: a protocol that offers the reliable transmission of packets. TCP’s job is to make sure that the packets leaving one device arrive on the other device in exactly the same order, and with no missing packets. If packet loss occurs, TCP re-transmits the packets. Packets behind the missing packet cannot be processed until retransmission occurs, so all assets being transferred on that single TCP connection are held up.</p>
<p>This is where we are currently at with GOV.UK. The vast majority of users on strong, stable connections will have seen improved performance from enabling HTTP/2. Unfortunately, some users may have seen performance drop if they use a connection with high packet loss. This is due to the HOL blocking that now exists at the network layer. This is a primary issue HTTP/3 sets out to solve.</p>
<h3 id="http3">HTTP/3</h3>
<p>To solve the HOL blocking issue at the network layer, HTTP/3 uses a completely new transport layer protocol called QUIC. QUIC initially stood for “Quick UDP Internet Connections”, but the IETF now don’t use it as an acronym. It is simply the name of the protocol. QUIC is built upon the User Datagram Protocol (UDP), so isn’t bound by the restrictions of TCP. It has been designed so that the network layer now understands the the concept of independent streams, which was restricted to the application layer under HTTP/2:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="900" height="532" class="figure__image" src="/images/rfc-139/QUIC-illustration-final.png" alt="Difference between TCP and QUIC when downloading assets." />
</picture>
</figure>
<p>In the illustration above, if packet 2 is lost (using HTTP/2), packets 3-8 are blocked until retransmission occurs. Using QUIC if packet 2 is lost only packets 1 and 3 are blocked since they exist in the same stream. The other streams can progress as normal since they are fully independent. QUIC doesn’t require in-order packet transmission across the whole connection, but it does still retain ordering within each resource stream.</p>
<h2 id="the-effect-of-packet-loss">The effect of packet loss</h2>
<p>Using WebPageTest we can roughly simulate the effect that packet loss has on performance for our users. For testing I picking a fairly content heavy page like the <a href="https://www.gov.uk/government/history/past-prime-ministers">past Prime Ministers</a> page and a low spec Moto G4 Android device. For the connection speed I’ve picked on one of the worse case scenarios from the <a href="https://docs.google.com/spreadsheets/d/1UTRucwLY2VC4944-cKunL9yUPJkjDKXDdIPk6GY3610/edit#gid=0">MLab data</a>. I’m emulating the connection the village of Colintraive has, which reportedly has a 450/230 Kbps connection speed. This is a connection speed equivalent to a 2G or slow 3G connection.</p>
<h3 id="visual-filmstrip">Visual filmstrip</h3>
<p>Below we can see a comparison of the page load under different packet loss conditions using HTTP/2. The top is the baseline, with 0% packet loss, progressing all the way down to 3% packet loss:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="2350" height="819" class="figure__image" src="/images/rfc-139/visual-filmstrip.png" alt="Comparing the visual filmstrip of the effect of packet loss on a page load" />
</picture>
</figure>
<p>As you can see, the visually complete metric sits at 9.5 seconds with no packet loss, but jumps to 13.5 seconds with 3% packet loss. This is a 42% increase.</p>
<h3 id="visual-progress-graph">Visual progress graph</h3>
<p>The visual progress graph clearly shows the effect packet loss has on page load times. As the packet loss increases, the page load slows down:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="898" height="398" class="figure__image" src="/images/rfc-139/visual-progress.png" alt="The visual progress graph comparing the percentage completeness of a page over time." />
</picture>
</figure>
<p>HTTP/3 + QUIC is designed to help improve the page load speed for users experiencing these connection conditions in the real world.</p>
<h3 id="connection-quality-measurement">Connection quality measurement</h3>
<p>Is it possible to measure the connection quality that our users are experiencing when visiting GOV.UK. Fastly provide a <a href="https://developer.fastly.com/reference/vcl/variables/client-connection/">range of Varnish Configuration Language (VCL) options</a> that we can use to monitor the quality of user connections should we wish to capture this data:</p>
<ul>
<li><a href="https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-ploss/">client.socket.ploss</a> - An estimate of the packet loss on the current connection. Provides a ratio from 0-1. 0 = no loss.</li>
<li><a href="https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-rtt/">client.socket.tcpi_rtt</a> - The round trip time (RTT) for the current TCP connection to the client. RTT is used for loss detection and congestion control across the connection.</li>
<li><a href="https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-rttvar/">client.socket.tcpi_rttvar</a> - Measures the variance in RTT of the connection. The higher the variance value, the less stable the connection.</li>
<li><a href="https://developer.fastly.com/reference/vcl/variables/client-connection/client-socket-tcpi-delivery-rate/">client.socket.tcpi_delivery_rate</a> - Gives a recent value for the delivery bandwidth to the client in bytes per second.</li>
</ul>
<h2 id="our-users">Our Users</h2>
<p>Since HTTP/3 + QUIC offers better performance for users on unstable connections, it is essentially fixing performance for those who were left behind during the transition to HTTP/2. These users are likely to be those who are in the most need of this performance increase.</p>
<p>Since COVID-19 many people have had to work from home. Many will be in rural areas where high-speed internet access either isn’t available, or is expensive. In more populated area many users could still be on ageing cable / DSL connections with <a href="https://www.broadbandchoices.co.uk/guides/broadband/what-is-contention-ratio">high contention ratios</a>.</p>
<p>The aim of this transition to HTTP/3 is to allow an ever more diverse level of user connection quality to access GOV.UK, essentially maximising access for all users, no matter what connection they use. By focusing on the long-tail of our users with poor connections, we can effectively shorten it and in doing so make the internet “good enough” to use in more situations. This is all without effecting performance for users already on a good internet connection.</p>
<h3 id="browser-support">Browser support</h3>
<p>The support for HTTP/3 + QUIC in browsers is visible on <a href="https://caniuse.com/?search=http3">Caniuse</a>.</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1369" height="499" class="figure__image" src="/images/rfc-139/http3-browser-usage.png" alt="HTTP/3 and QUIC browser usage screenshot from CanIUse.com." />
</picture>
</figure>
<p>The browsers support for HTTP/3 at the time of writing is:</p>
<ul>
<li>Chrome - Enabled (see <a href="https://blog.chromium.org/2020/10/chrome-is-deploying-http3-and-ietf-quic.html">Chromium blog</a>)</li>
<li>Edge (chromium version) - Enabled</li>
<li>Firefox - Enabled in the next version (88)</li>
<li>Safari 14 - Disabled (currently behind a feature flag)</li>
</ul>
<p>Looking at the March 2021 analytics, approximately 55% of GOV.UK users are using a browser that can now take advantage of HTTP/3 + QUIC. This is close to the 66% that the CanIUse data estimates for HTTP/3 usage globally.</p>
<h2 id="usage-on-the-web">Usage on the web</h2>
<p>Although the HTTP/3 and QUIC specifications are still in draft, they are both used in production on large sites with billions of daily page views:</p>
<h3 id="youtube">YouTube</h3>
<p>In the image below we see Chrome 89 (latest stable) connecting to YouTube and using <code class="language-plaintext highlighter-rouge">h3-Q050</code>. This is HTTP/3 with the latest version of Google QUIC (<code class="language-plaintext highlighter-rouge">Q050</code>).</p>
<figure class="figure">
<picture>
<img loading="lazy" width="673" height="688" class="figure__image" src="/images/rfc-139/youtube-h3.png" alt="YouTube using HTTP/3 + QUIC as the transport protocol." />
</picture>
</figure>
<h3 id="google-search">Google Search</h3>
<p>In the image below we see Chrome 89 (latest stable) connecting to Google Search and using <code class="language-plaintext highlighter-rouge">h3-Q050</code>. This is HTTP/3 with the latest version of Google QUIC (<code class="language-plaintext highlighter-rouge">Q050</code>).</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1052" height="485" class="figure__image" src="/images/rfc-139/google-search-h3.png" alt="Google Search using HTTP/3 and QUIC." />
</picture>
</figure>
<h3 id="facebook">Facebook</h3>
<p>Facebook are using HTTP/3 and QUIC in production, but are using the IETF version of QUIC (<code class="language-plaintext highlighter-rouge">h3-29</code>). Below we see Firefox 88 connecting via HTTP/3:</p>
<figure class="figure">
<picture>
<img loading="lazy" width="1876" height="1154" class="figure__image" src="/images/rfc-139/fb-h3.png" alt="Facebook servers are also HTTP/3 and QUIC enabled." />
</picture>
</figure>
<h2 id="other-advantages-to-http3--quic">Other advantages to HTTP/3 + QUIC</h2>
<p>I’ve only covered a single advantage related to packet loss with web performance above, but HTTP/3 + QUIC comes with a number of other advantages:</p>
<ul>
<li>Improvement in connection startup times via TLS v1.3 + <a href="https://blog.cloudflare.com/introducing-0-rtt/">0-RTT</a></li>
<li>Better security via end-to-end encryption at the transport layer</li>
<li>Establish an ecosystem that is easier to evolve from our current TCP usage</li>
</ul>
<p>Eventually our users will also be able to take advantage of <a href="https://tools.ietf.org/html/draft-paulo-quic-migration-00">Connection Migration</a>, where QUIC end-points will be able to migrate connections to different IP addresses and network paths at will.</p>
<p>Connection migration is an optional part of the specification that isn’t currently implemented anywhere. Once enabled it will allow a users device to seamlessly transition between multiple QUIC connections (e.g. Cellular to known Wi-fi). This is achieved by introducing the concept of a connection ID that is carried by QUIC packets, which can be used to identify a connection.</p>
<p>A user is likely to find this useful if they are live streaming data (web conferencing, large file upload / download, real-time navigation etc). A live demo of this technology in cation can be <a href="https://www.youtube.com/watch?v=In1aALwdahY">found here</a>.</p>
<p>Since this is optional in the QUIC specifications, realistically its practical adoption may be a number of years away.</p>
<h2 id="summary-1">Summary</h2>
<p>Our move to HTTP/3 + QUIC offers web performance gains to users on unstable, high packet loss connections. These users were left behind in the migration to HTTP/2 across the web platform. These web performance gains will come with no loss in performance to users already on a strong, stable connection. All our users will have an improvement in security, as well as improvements in connection startup times.</p>
<p>It has never been more important to fix issues for our long-tail users, since many will now be working from home due to the <a href="https://www.gov.uk/coronavirus">Coronavirus (COVID‑19)</a>. Users no longer working in an office environment may now be limited to unstable connections in rural areas, or tethering via limited mobile data plans, or using a connection in populated inner-city area with a high local contention ratios.</p>
<p>Any browsers that don’t support HTTP/3 + QUIC will fallback to HTTP/2 + TCP. Therefore HTTP/3 can be enabled using the progressive enhancement methodology.</p>
<h2 id="proposal">Proposal</h2>
<p>Fastly have now announced HTTP/3 + QUIC is available to all customers as it is out of <a href="https://www.fastly.com/blog/http-3-and-quic-are-now-available-for-our-entire-customer-base-at-no-additional-charge">Limited Availability (LA) and the beta phases</a>. Enabling HTTP/3 + QUIC for us involves switching it on via the Fasty UI. No modifications are needed to our application code, or servers. And since we have already invested work in optimising for HTTP/2, we are already well prepared for a migration to HTTP/3. So no modifications are needed to our frontend applications.</p>
<p>There is a CNAME config change required if we don’t use a dedicated Fastly IP address once enabled in the UI. From the <a href="https://docs.fastly.com/en/guides/enabling-http3-for-fastly-services#sending-your-http3-traffic-to-fastly">documentation</a>:</p>
<blockquote>
<p>Unless you use Fastly’s dedicated IP addresses, then as a final step to enabling HTTP/3, you must ensure the DNS records of your domains are routing users to the correct HTTP/3 enabled Fastly addresses by using one of the following CNAMEs:</p>
</blockquote>
<table>
<thead>
<tr>
<th>0RTT enabled?</th>
<th>IPv4 and IPv6 dual stack routing</th>
<th>IPv4-only routing</th>
</tr>
</thead>
<tbody>
<tr>
<td>Yes</td>
<td><code class="language-plaintext highlighter-rouge">dualstack.n.sni.global.fastly.net</code></td>
<td><code class="language-plaintext highlighter-rouge">n.sni.global.fastly.net</code></td>
</tr>
<tr>
<td>No</td>
<td><code class="language-plaintext highlighter-rouge">dualstack.m.sni.global.fastly.net</code></td>
<td><code class="language-plaintext highlighter-rouge">m.sni.global.fastly.net</code></td>
</tr>
</tbody>
</table>
<p>I know GOV.UK currently has IPv6 enabled, so if a CNAME change is required I propose we use 0-RTT = No (<code class="language-plaintext highlighter-rouge">dualstack.m.sni.global.fastly.net</code>), since I belive Firefox is still the only browser that supports it. We can always enable this at a later date quite easlily once it becomes more promenant in browsers.</p>
<p>Note: According to Richard Towers, we currently don’t use dedicated Fastly IP’s so the CNAME change will be required, this shouldn’t be a major issue because we have a couple of levels of CNAME in place (we don’t need to talk to JISC). It’s also worth noting that TTL on prod is 300 seconds (5 mins), which should be short enough for the change when made.</p>
<h2 id="date-to-be-enabled">Date to be enabled</h2>
<p>I’ve opened an additional <a href="https://github.com/alphagov/govuk-rfcs/blob/103acd22a74b32d4ea9dd321bc0ab05ac664aa82/rfc-147-enable-speedcurve-http-protocol-capture.md">RFC-147</a>, that will allow us to distinguish users using HTTP/2 and HTTP/3 in SpeedCurve RUM. I believe we should impliment this RFC first then allow it to capture user data for 1-2 months, before we enable HTTP/3. This will allow us to quantify the before / after change in our RUM metrics, which would make for an interesting PR piece from a web performance and user focus point of view. So I’d propose to enable HTTP/3 some time in July / August 2022.</p>
<h2 id="http3-enabled-on-govuk">HTTP/3 enabled on GOV.UK!</h2>
<p>I’m very pleased to say that HTTP/3 + QUIC from Fastly was enabled on GOV.UK on the 11th January 2024! See the tweet thread <a href="https://twitter.com/TheRealNooshu/status/1745843848809484419">here</a> for more information.</p>Matt HobbsThis is an RFC I wrote to persuade the GOV.UK Senior Technology team to enable HTTP/3 on GOV.UK. It was accepted and merged into the repo, and as of 11th Jan 2024 it has now been enabled!Migrating from Github Pages to Netlify & Cloudflare2021-09-06T01:09:00+00:002021-09-06T01:09:00+00:00https://nooshu.com/blog/2021/09/06/migrating-from-github-pages-to-cloudflare-and-netlify<p>So in recent months I’ve had a <a href="/blog/2021/05/12/weve-spotted-something-on-your-scan/">fair bit going on in my life</a> which has held me back in many ways, but also pushed a few things up the priority list too. One of the areas that has been bumped up the list is the current setup of this very blog. So I decided to clean up the years of “it still works, why would I change it”, technical debt. Isn’t it incredible how many years you can do this for!</p>
<h2 id="dreamhost">Dreamhost</h2>
<p>Just a very brief look back on the history of the blog and allow me to vent a little. Feel free to jump to the <a href="#github-pages">next section</a> if this is of no interest to you.</p>
<p>I started using Dreamhost for my hosting & domain registrar back in April 2006, just over 15 years ago! At the time this was because they offered a lot in terms of functionality all rolled into one. You could easily purchase your domain and set up the hosting quickly. So this became my default option for friends and families websites. The initial version of the blog used WordPress (~v2.0). At the time this was a great way to learn how to work with PHP/MySQL, and WordPress felt like the only real quick ‘self-hosted’ option available (I’m sure many will disagree with that statement).</p>
<p>Over the years my Dreamhost control panel just filled with random domains and projects that weren’t maintained. So in the past 3 months I decided to clean this all up. Remove all the old unused domains, and migrate any friends’ websites over to other hosting options, so I’d no longer need to pay for a service I really don’t need. My plan was to try and get friends to open their own Dreamhost account and basically get the Dreamhost team to do an ‘internal transfer’ between accounts. Simple right?!</p>
<p>So I contacted Dreamhost telling them my current situation and my plan. Their response was basically: “Sorry to hear of your situation…sure we can help you do that… for this one time admin fee of $99.00 USD…”. Isn’t capitalism great! Not that I’d expect it to be free, but after 15 years and the fact that I’m trying to get friends to sign up and pay for their services moving forwards, you’d have thought they would have the foresight to waive the fee.</p>
<p>I neither had the drive or the mental capacity to argue the point so left it at that and decided that Dreamhost isn’t a company I want to be dealing with anymore, especially not with all the other options available on the market now! So time to transfer everything away, including them as a domain registrar, which is pretty much all it’s been used for with this blog for many years anyway.</p>
<h2 id="github-pages">Github pages</h2>
<p>This blog is built using the static site generator called <a href="https://jekyllrb.com/">Jekyll</a>. It works okay, but can be a bit of a dependency hell when updating as it is all built on Ruby. The great thing about this setup is that it integrates really well with Github (GH) Pages. There’s a whole guide on how to set up a new site really quickly <a href="https://pages.github.com/">here</a>. So this is what I did for many years. I didn’t even bother setting up a custom domain, the blog just existed on <code class="language-plaintext highlighter-rouge">nooshu.github.io</code> (more on this later). Once configured, just write your posts in <a href="https://daringfireball.net/projects/markdown/">Markdown</a>, push your changes to the repository and GH pages will handle the rest.</p>
<p>The downside with this setup is:</p>
<p>It’s fairly limited. You can only use specific Jekyll plugins supported by GH pages. So you can’t take advantage of all plugins available.
It’s a paid for only option if using a private repository (which my blog is). I don’t want to change this restriction.</p>
<p>In all honesty it’s mainly the paid for option that I’m looking to remove. Not to be too blunt about it all, but should anything happen in the coming months, future payments wouldn’t be made and the blog would soon be taken offline.</p>
<h2 id="netlify">Netlify</h2>
<p>This is where <a href="https://www.netlify.com/">Netlify</a> steps in. Netlify is a Git-based workflow and powerful serverless platform that can be used to build, deploy, and collaborate on web apps. It comes with a fantastic ‘Starter’ plan for personal projects, hobby sites, or experiments. I can’t even tell you how simple it is to set up a site if you’re already using Jekyll. Literally point Netlify towards your site repository, a minute or so later you’ll have yourself an exact copy of the site (although on a randomly assigned URL, but we will get to this part soon). If this hasn’t convinced you to give it a try, it is also been supported and maintained by a fantastic bunch of amazing developers like:</p>
<ul>
<li><a href="https://twitter.com/sarah_edo">Sarah Drasner</a></li>
<li><a href="https://twitter.com/zachleat">Zach Leatherman</a></li>
<li><a href="https://twitter.com/philhawksworth">Phil Hawksworth</a></li>
</ul>
<p>Now technically you <em>could</em> pretty much stop there. Let Netlify handle the custom domain, DNS, Hosting, SSL, deployment etc as <a href="https://www.netlify.com/blog/2017/03/28/why-you-dont-need-cloudflare-with-netlify/">you don’t actually need Cloudflare</a>. But where’s the fun in that?? So let’s layer on Cloudflare anyway!</p>
<h2 id="cloudflare">Cloudflare</h2>
<p><a href="https://www.cloudflare.com/">Cloudflare</a> are one of the big market leaders when it comes to Content Distribution Networks (CDN) on the web. But don’t let their huge size put you off, they still have an excellent <a href="https://www.cloudflare.com/plans/">free plan</a> that anyone can use to speed up their website. It’s incredible the features they offer on their free plan:</p>
<ul>
<li><a href="https://blog.cloudflare.com/http3-the-past-present-and-future/">HTTP/3 + QUIC</a></li>
<li><a href="https://blog.cloudflare.com/introducing-0-rtt/">0-RTT</a></li>
<li><a href="https://workers.cloudflare.com/">Cloudflare Workers</a></li>
<li><a href="https://www.cloudflare.com/en-gb/features-page-rules/">Page Rules</a></li>
</ul>
<p>There really is so much there for a standard website or a hobbyist looking to do some <a href="/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/">web performance experimentation</a>.</p>
<p>Another bonus is that they can also act as a <a href="https://www.cloudflare.com/en-gb/products/registrar/">domain registrar</a> either for new domains, or to transfer existing domains to them. So this is what I’ve done. My Dreamhost domains have now all been transferred to Cloudflare and are all in a single place.</p>
<h2 id="setting-it-all-up">Setting it all up</h2>
<p>Enough of the babbling, let’s dig into setting it all up!</p>
<h3 id="domains">Domains</h3>
<p>First thing I did was to start the domain transfer process. Dead simple to do. Make sure the domain is unlocked, grab the auth code from the previous registrar then wait 5 days… It’s annoying that you wait 5 days for this but Dreamhost doesn’t give you an option to bypass the ‘cooling off’ period. It would be great if there was an option to start the transfer process right away, but alas it is what it is. I did this via <a href="https://www.cloudflare.com/en-gb/products/registrar/">Cloudflare Registrar</a>, but any will work in mostly the same way.</p>
<h3 id="netlify-1">Netlify</h3>
<p>As mentioned above, Netlify is so simple to get setup. Click the “New site from git” button, select your repo and configure your build settings.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/netlify-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/netlify-1.avif 1538w" type="image/avif" />
<img loading="lazy" width="1538" height="168" class="figure__image" src="/images/netlify-cloudflare-migration/netlify-1.png" alt="New site from Git button image on Netlify." />
</picture>
</a>
</figure>
<p>Clicking the button will bring up the following options:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/netlify-2.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/netlify-2.avif 1510w" type="image/avif" />
<img loading="lazy" width="1510" height="952" class="figure__image" src="/images/netlify-cloudflare-migration/netlify-2.png" alt="Create a new site dialog with Netlify." />
</picture>
</a>
</figure>
<p>A few minutes later you’ll have a new site on a random Netlify URL that you can easily access.</p>
<h3 id="ssl-certificate--dns">SSL Certificate + DNS</h3>
<p>This is where it got slightly more complicated. By default you could leave Netlify to handle the DNS settings and SSL certificate, but since we want to front our site with Cloudflare we have to make a few changes.</p>
<p><strong>SSL Certificate</strong></p>
<p>So we aren’t going to use Netlify to provision the SSL certificate, we are going to be using Cloudflare. This is important if you want to use the CDN functionality of Cloudflare. As it is the “front” of the website, we are going to be using Netlify as the origin server.</p>
<p>An SSL certificate provisioned by cloudflare will allow encryption of traffic between the origin (Netlify) and the CDN (Cloudflare). I <em>believe</em> this is also important when it comes to using the HTTP/3 + QUIC functionality that Cloudflare offers on their free plan. This is because TLS is very much <a href="https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/6ef36d1e-d91e-43e0-8732-f3e66ba9ea64/protocol-stack-h2-h3.png">embedded within the QUIC protocol</a>, rather than a separate layer like it is in HTTP/2. So if Netlify were to handle the SSL certificate, it would limit our ability to use HTTP/3. <strong>Note</strong>: this is just an educated guess of mine. As I could only get HTTP/3 + QUIC to work when Cloudflare provisioned the SSL certificate.</p>
<p>So first you need to generate an origin certificate in the Cloudflare control panel for your domain:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/cert-2.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/cert-2.avif 2122w" type="image/avif" />
<img loading="lazy" width="2122" height="886" class="figure__image" src="/images/netlify-cloudflare-migration/cert-2.png" alt="Create origin certificate in Cloudflare." />
</picture>
</a>
</figure>
<p>I enabled Full (strict) mode here that requires the Cloudflare CA certificate.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/cert-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/cert-1.avif 2098w" type="image/avif" />
<img loading="lazy" width="2098" height="1388" class="figure__image" src="/images/netlify-cloudflare-migration/cert-1.png" alt="Setting the Cloudflare to Full (strict) SSL settings." />
</picture>
</a>
</figure>
<p>Next you need to enter this certificate info into Netlify via the “Site settings → Domain Management → HTTPS”. Click the ‘Add (or Update) custom certificate’.</p>
<p>Install your custom certificate by copy & pasting the corresponding fields into the textareas provided. You will also need to provide the ‘intermediate’ certificate details. For this, go to the <a href="https://developers.cloudflare.com/ssl/origin-configuration/">Cloudflare Origin configuration page</a> and download the <a href="https://developers.cloudflare.com/ssl/origin-configuration/origin-ca#4-required-for-some-add-cloudflare-origin-ca-root-certificates">Cloudflare Origin RSA PEM</a> file and open it in your favorite text editor. Copy & paste the contents into the last textarea.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/https-2.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/https-2.avif 1260w" type="image/avif" />
<img loading="lazy" width="1260" height="1270" class="figure__image" src="/images/netlify-cloudflare-migration/https-2.png" alt="Install custom certificate dialog in Netlify." />
</picture>
</a>
</figure>
<p>Once completed your HTTPS settings in Netlify should look similar to this:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/https-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/https-1.avif 1904w" type="image/avif" />
<img loading="lazy" width="1904" height="1088" class="figure__image" src="/images/netlify-cloudflare-migration/https-1.png" alt="HTTPS settings in Netlify once the certificate is installed." />
</picture>
</a>
</figure>
<p>That’s about it for SSL certificates. You’ve now installed the Cloudflare origin certificate on your origin server (Netlify) so they can talk to each other over an encrypted connection.</p>
<p><strong>DNS</strong></p>
<p>Okay, here’s the part that I don’t mind admitting took me bloody ages to figure out! When it comes to Domain Name System (DNS) I know enough to be dangerous, but not enough to be useful! So there was a fair amount of head scratching and trial and error to get this all setup. It doesn’t help that these settings take a while to propagate across the internet. So you never know if they are still propagating, or if you just broke something! For ease, here are the settings I currently have in place in my Cloudflare DNS panel:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/dns-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/dns-1.avif 2094w" type="image/avif" />
<img loading="lazy" width="2094" height="502" class="figure__image" src="/images/netlify-cloudflare-migration/dns-1.png" alt="DNS record settings in Cloudflare." />
</picture>
</a>
</figure>
<p>First we have an <code class="language-plaintext highlighter-rouge">A</code> record pointing to Netlify’s load balancer IP address (<code class="language-plaintext highlighter-rouge">75.2.60.5</code>). More info on this is available on their “<a href="https://docs.netlify.com/domains-https/custom-domains/configure-external-dns/#configure-an-apex-domain">Configure external DNS for a custom domain</a>” page.</p>
<p>Second we have a <code class="language-plaintext highlighter-rouge">CNAME</code> record pointing <code class="language-plaintext highlighter-rouge">www</code> to <code class="language-plaintext highlighter-rouge">nooshu.com</code>. I don’t want the ‘www’ in my URL, so it is just being pointed to the bare domain (<code class="language-plaintext highlighter-rouge">https://nooshu.com</code>).</p>
<p>Note that for both of these records the ‘Proxy Status’ column is set to ‘Proxied’ (notice the orange cloud). This is important as we want to make sure the hostname is running through Cloudflare as our CDN provider.</p>
<p>Under this panel I pointed the domain to the Cloudflare Nameservers (<code class="language-plaintext highlighter-rouge">braelyn.ns.cloudflare.com</code> and <code class="language-plaintext highlighter-rouge">cartman.ns.cloudflare.com</code>). Finally in the DNS page I enabled <a href="https://www.icann.org/resources/pages/dnssec-what-is-it-why-important-2019-03-05-en">DNSSEC</a>. This isn’t required, but it adds a layer of extra security by protecting against forged DNS answers. It comes for free with the free plan, so may as well I guess! (Unless anyone knows otherwise?)</p>
<p>That’s it for the Cloudflare DNS page! A little word or warning: if you go back to the Netlify ‘Domain Management’ panel you will see some scary looking warning messages next to your custom domains.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/netlify-dns.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/netlify-dns.avif 1872w" type="image/avif" />
<img loading="lazy" width="1872" height="546" class="figure__image" src="/images/netlify-cloudflare-migration/netlify-dns.png" alt="Netlify DNS settings with errors that can be ignored." />
</picture>
</a>
</figure>
<p>These are fine to ignore. It’s simply because the domain’s DNS isn’t being handled by Netlify, it’s been handled by Cloudflare.</p>
<h3 id="netlify-forms">Netlify Forms</h3>
<p>This is an optional extra. If your site requires a contact form you can’t go wrong with Netlify. It comes with the ability to manage forms and submissions without any server-side code or JavaScript. It’s all done via <a href="https://www.netlify.com/products/forms/">Netlify Forms</a>. It is dead simple to set up and comes with built-in spam protection via <a href="https://docs.netlify.com/forms/spam-filters/">Akismet and an optional honeypot field</a>. All for free (up to a certain usage limit).</p>
<h3 id="email">Email</h3>
<p>Next up is email setup. Ideally I wanted <code class="language-plaintext highlighter-rouge">name@domain.com</code> to forward to my GMail account, but Cloudflare doesn’t <a href="https://community.cloudflare.com/t/cloudflare-domains-email-forwarding/197040/2">handle emails at all</a>. Thankfully I found an excellent free service to do this called <a href="https://improvmx.com/">ImprovMX</a>. Simply enter the alias you require and the email address you want to forward too, then follow the instructions given. You enter all these details into your Cloudflare DNS panel. An example of my setup is below:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/email-setup.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/email-setup.avif 2112w" type="image/avif" />
<img loading="lazy" width="2112" height="326" class="figure__image" src="/images/netlify-cloudflare-migration/email-setup.png" alt="Cloudflare DNS settings for setting up an email address." />
</picture>
</a>
</figure>
<p>That’s all there is to that really. Quick and simple!</p>
<h3 id="redirects">Redirects</h3>
<p>This was a big concern for me at first. My blog has been on the default Github domain for a number of years (<code class="language-plaintext highlighter-rouge">nooshu.github.com</code>). So what would happen if I suddenly changed to <code class="language-plaintext highlighter-rouge">nooshu.com</code>? Would all the links built up over the years just break? Github pages is very limited as to what you can do technically. It’s not like it uses Apache for example, where you can use something like <code class="language-plaintext highlighter-rouge">mod_rewrite</code> to create redirects.</p>
<p>Thankfully Github does come with some useful functionality to help in this situation. Navigate to ‘Settings → Pages → Custom domain’ page. Enter the domain you wish to serve the site from into the input box. Behind the scenes this will create a <code class="language-plaintext highlighter-rouge">CNAME</code> file in the root of the repository that will redirect users to the new domain.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/gh-domain.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/gh-domain.avif 1866w" type="image/avif" />
<img loading="lazy" width="1866" height="230" class="figure__image" src="/images/netlify-cloudflare-migration/gh-domain.png" alt="Setting the custom domain in the Github panel." />
</picture>
</a>
</figure>
<p>So in my case <code class="language-plaintext highlighter-rouge">nooshu.github.com</code> redirects to <code class="language-plaintext highlighter-rouge">nooshu.com</code> automatically. Assuming you keep the rest of your URL structure the same, you should be all good to go!</p>
<h3 id="web-mentions">Web Mentions</h3>
<p>I use <a href="https://www.w3.org/TR/webmention/">Webmentions</a> on my blog. I actually wrote about how I do it <a href="/blog/2019/11/18/implementing-webmentions/">here</a>. The ‘issue’ with using web mentions via the <a href="https://webmention.io/">Webmention.io service</a> is that it is tied to a particular domain. But what happens once the domain is changed? Suddenly all the webmentions that were associated with <code class="language-plaintext highlighter-rouge">nooshu.github.io</code> are no longer pulled in since the blog has moved to <code class="language-plaintext highlighter-rouge">nooshu.com</code>. Thankfully after a brief chat with <a href="https://twitter.com/kevinmarks">Kevin Marks</a> on Twitter a simple solution was found.</p>
<p>In my JavaScript I’m querying for any webmentions that are associated with the <code class="language-plaintext highlighter-rouge">nooshu.github.io</code> domain. The <code class="language-plaintext highlighter-rouge">webmention.io</code> API allows you to query multiple domains at the same time. So I just <a href="https://gist.github.com/Nooshu/521c754d1a0da7b7917351fa6b22886c#file-webmention-js-L36">modified my JavaScript</a> to look for mentions from both domains. This allows for mentions from the past (<code class="language-plaintext highlighter-rouge">nooshu.github.io</code>) and also any mentions in the future (<code class="language-plaintext highlighter-rouge">nooshu.com</code>) to be collected and parsed. Simple!</p>
<h2 id="web-performance">Web Performance</h2>
<p>This final section of the post was the really interesting part for me. How to get the best performance out of the new Netlify + Cloudflare setup. It’s worth mentioning upfront that by default it really <strong>isn’t</strong> optimal. Here’s the WebPageTest result for this site before any changes were made. Here I’m testing on a real Moto G4 on a 3G Fast connection. So essentially a low spec device on a slow connection.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/before-score-result.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/before-score-result.avif 2204w" type="image/avif" />
<img loading="lazy" width="2204" height="434" class="figure__image" src="/images/netlify-cloudflare-migration/before-score-result.png" alt="WebPageTest score from before all the tweaks were applied to the blog." />
</picture>
</a>
</figure>
<p>So the <a href="https://www.webpagetest.org/result/210831_AiDcQR_1c8c05bd7a25ec9c714a81bc31a84296/">test result</a> isn’t actually too bad in terms of performance. A Largest Contentful Paint (LCP) and visually complete of around 2 seconds on a low spec device is a good place to start. But there are three scores that stand out:</p>
<ul>
<li>Security: E</li>
<li>Cache static content: F</li>
<li>First Byte: B</li>
</ul>
<p>All these scores are very much intertwined, but I will go through each of them individually the best I can..</p>
<h3 id="security">Security</h3>
<p>Since this is a small static blog with no admin area or any form of login, security isn’t a major issue. But it would be nice to get an ‘A’ in this anyway. Thankfully it really isn’t that hard to do if you are using Cloudflare as a CDN. Security expert <a href="https://twitter.com/Scott_Helme">Scott Helme</a> covers most of what to do in his blog post “<a href="https://scotthelme.co.uk/security-headers-cloudflare-worker/">The brand new Security Headers Cloudflare Worker</a>”. If you are looking for a guide on setting up a Cloudflare Worker, I’ve written a <a href="/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/">post here</a> you can follow. The issue with the score is that the site is being served with some missing security headers:</p>
<p><strong>Note</strong>: There’s no need to add these missing headers using a Worker anymore as Cloudflare has just released some new functionality called <a href="https://blog.cloudflare.com/transform-http-response-headers/">“HTTP response headers with Transform Rules”</a>, see my <a href="#using-http-response-headers-with-transform-rules">update below</a> on how to do this.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/security-headers-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/security-headers-1.avif 2454w" type="image/avif" />
<img loading="lazy" width="2454" height="526" class="figure__image" src="/images/netlify-cloudflare-migration/security-headers-1.png" alt="Security score came out as a D when automatically scanned." />
</picture>
</a>
</figure>
<p>As you can see from the scan above by <a href="https://securityheaders.com/">securityheaders.com</a>, we are missing 5 security headers (in red). By following Scott’s guide and a <a href="https://scotthelme.co.uk/goodbye-feature-policy-and-hello-permissions-policy/">small tweak related to ‘Feature-Policy` header</a> you can easily get it up to an A+. Here’s the headers I have included in my Cloudflare Worker:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">securityHeaders</span> <span class="o">=</span> <span class="p">{</span>
<span class="dl">"</span><span class="s2">Content-Security-Policy</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">upgrade-insecure-requests</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">Strict-Transport-Security</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">max-age=31536000; preload; includeSubDomains</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">X-Xss-Protection</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">1; mode=block</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">X-Frame-Options</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">DENY</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">X-Content-Type-Options</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">nosniff</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">Referrer-Policy</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">strict-origin-when-cross-origin</span><span class="dl">"</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">Permissions-Policy</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">fullscreen=(), geolocation=(), camera=()</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This code, combined with Scott’s boilerplate code adds the headers to the HTML response from the CDN. Now you don’t necessarily need to use a Worker to add all these headers. You could use functionality that <a href="https://docs.netlify.com/routing/headers/">Netlify has available</a> where you can modify the headers coming from the origin using a <code class="language-plaintext highlighter-rouge">_headers</code> or <code class="language-plaintext highlighter-rouge">netlify.toml</code> file. But I didn’t have much luck with this route. Certain headers were being stripped somewhere in the process so I eventually just reverted to using a Worker to do it. <a href="http://twitter.com/SimonHearne">Simon Hearne</a> has an <a href="https://simonhearne.com/2019/http-headers-fast-and-secure/#configuring-headers-on-netlify">excellent blog post</a> all about this, where he managed to get it to work if you are interested. The above setup will also allow you to submit your domain to the <a href="https://hstspreload.org/">HSTS Preload list</a> too, ensuring that the domain (and all subdomains) are only accessible with a valid SSL certificate.</p>
<p>Once the worker is appending the missing headers to your HTML response the score jumps from an E to an A+! Star pupil indeed! All using a Cloudflare Worker that comes for free on the free plan. Win-win all round!</p>
<h3 id="using-http-response-headers-with-transform-rules">Using HTTP response headers with Transform Rules</h3>
<p>As mentioned above Cloudflare have just relesed functionality that allows you to easily modify response headers without the need of a Worker. It’s all done via the Cloudflare UI.</p>
<p>Just select your site then navigate to ‘Rules’ –> ‘Transform Rules’. Then create a new set of response rules like below:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/transform-rules-1v2.png">
<picture>
<img loading="lazy" width="1856" height="436" class="figure__image" src="/images/netlify-cloudflare-migration/transform-rules-1v2.png" alt="Add the response headers when a GET request is seen." />
</picture>
</a>
</figure>
<p><strong>Note</strong>: You <a href="https://twitter.com/rboulton/status/1469062262061154315">can’t currently use the expression builder to target only the HTML response</a>, but you can paste in the following expression if you only want to target a specific <code class="language-plaintext highlighter-rouge">content-type</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http.request.method eq "GET" and any(http.response.headers["content-type"][*] contains "text/html")
</code></pre></div></div>
<p>Also worth nothing that this feature is on the list to be <a href="https://twitter.com/rboulton/status/1469073236948303875">added to the expression builder</a>. If you don’t only target your HTML, your finely crafted <code class="language-plaintext highlighter-rouge">Cache-Control</code> asset headers will be decimated and all overwritten with <code class="language-plaintext highlighter-rouge">public, max-age=60</code>.</p>
<p>Then I’ve simply added 9 static rules to the response from Cloudflare:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/transform-rules-2.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/transform-rules-2.avif 2178w" type="image/avif" />
<img loading="lazy" width="2178" height="1096" class="figure__image" src="/images/netlify-cloudflare-migration/transform-rules-2.png" alt="The 9 static response headers I've added can be seen in this screenshot. They are listed as text below if required." />
</picture>
</a>
</figure>
<p>The headers I added can be found below, the top 8 bump the security score from E to A+ on <a href="https://snyk.io">Snyk.io</a>. The final header is a specific preload header for this blog and can be ignored.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/** Important security related headers**/
Cache-Control: public, max-age=60
Content-Security-Policy : upgrade-insecure-requests
Strict-Transport-Security : max-age=2592000
X-Xss-Protection : 1; mode=block
X-Frame-Options : DENY
X-Content-Type-Options : nosniff
Referrer-Policy : strict-origin-when-cross-origin
Permissions-Policy: fullscreen=(), geolocation=(), camera=()
/**Specific preload for my blog**/
Link: </images/app-shell/grid.png>; rel=preload; as=image; nopush, </images/app-shell/bubble.svg>; rel=preload; as=image; nopush
</code></pre></div></div>
<p>Once this is done you can deploy and strip all the headers from the worker that exist in the <code class="language-plaintext highlighter-rouge">securityHeaders</code> object in the Worker code. Done!</p>
<p>Many thanks to the Cloudflare team who quickly removed a restriction with the <code class="language-plaintext highlighter-rouge">Strict-Transport-Security</code> header <a href="https://twitter.com/rboulton/status/1463181949355368449">in a couple of hours</a>. Amazing work!</p>
<h3 id="cache-static-content">Cache static content</h3>
<p>This is where I first spotted a real performance issue, and many thanks to <a href="https://twitter.com/anthony_ricaud">Anthony Ricaud</a> and <a href="https://twitter.com/AndyDavies">Andy Davies</a> for helping me solve this issue. Logging into the Cloudflare dashboard, on the ‘overview’ page it gives you a whole set of useful graphs. The interesting one was the ‘Percent Cached’ graph:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/cache-hit-ratio-1.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/cache-hit-ratio-1.avif 1556w" type="image/avif" />
<img loading="lazy" width="1556" height="238" class="figure__image" src="/images/netlify-cloudflare-migration/cache-hit-ratio-1.png" alt="Cache hit ratio in Cloudflare without any tweaks sits at 2%." />
</picture>
</a>
</figure>
<p>As you can see from the graph above, it’s sitting around a 2% cache rate. For a static site this is truly shocking. There’s literally nothing dynamic going on with any of the pages. Why was the cache hit percentage so terrible?</p>
<p>To help debug the issue I used a handy feature that Chrome has built-in, where you can expose a <a href="https://twitter.com/TheRealNooshu/status/1433475671859384325">custom header in the browsers network tab</a>. In the case of Cloudflare you want to be exposing the <code class="language-plaintext highlighter-rouge">cf-cache-status</code> header. There’s an explanation of what this header means <a href="https://getfishtank.ca/blog/cloudflare-cdn-cf-cache-status-headers-explained">here</a>. A quick TL;DR; is that we want as many of our assets to have a value of <code class="language-plaintext highlighter-rouge">HIT</code>, which means the asset is stored on the CDN and served to our user from their closest Point of Presence (PoP). As you can see from the image below, we were far from the case!</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/cache-hit-ratio-2.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/cache-hit-ratio-2.avif 528w" type="image/avif" />
<img loading="lazy" width="528" height="604" class="figure__image" src="/images/netlify-cloudflare-migration/cache-hit-ratio-2.png" alt="You can see why the cache hit percentage is so poor by examining the network tab and the cf-cache-status header." />
</picture>
</a>
</figure>
<p>There’s not a single <code class="language-plaintext highlighter-rouge">HIT</code> in the above image! No wonder the graph percentage was so low! So what’s going on here? Well this is where we have a collision between the Netlify and Cloudflare headers. By default many of the assets served by Netlify come with a <code class="language-plaintext highlighter-rouge">Cache-Control</code> header value of <code class="language-plaintext highlighter-rouge">public, max-age=0, must-revalidate</code>.</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/header-values-default.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/header-values-default.avif 1774w" type="image/avif" />
<img loading="lazy" width="1774" height="598" class="figure__image" src="/images/netlify-cloudflare-migration/header-values-default.png" alt="These are the default header values coming from the Netlify origin." />
</picture>
</a>
</figure>
<p>Using <a href="https://twitter.com/jaffathecake">Jake Archibald</a>’s “<a href="https://jakearchibald.com/2016/caching-best-practices/">Caching best practices & max-age gotchas</a>” and <a href="https://twitter.com/csswizardry">Harry Roberts</a> handy “<a href="https://csswizardry.com/2019/03/cache-control-for-civilians/">Cache-Control for Civilians</a>” we can start to make sense of these somewhat confusing header values:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">public</code> - this is good, no issue here.</li>
<li><code class="language-plaintext highlighter-rouge">max-age=0</code> - This is bad, it <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#preventing_caching">forces the cache to revalidate</a> (clears the cache).</li>
<li><code class="language-plaintext highlighter-rouge">must-revalidate</code> - Here we are forcing the browser to go back to the server to revalidate and check for a new resource, Harry writes about it in detail <a href="https://csswizardry.com/2019/03/cache-control-for-civilians/#must-revalidate">here</a>.</li>
</ul>
<p>The important thing to realise is that even though Cloudflare is fronting the domain, it still looks at, and in many cases, just forwards these headers to the user. So Netlify is effectively asking the CDN not to store the asset and go back to the origin to revalidate the asset on every request. This isn’t exactly what we want! This puts load on the origin and also adds to our users Time to First Byte (TTFB), since their nearest PoP isn’t being used.</p>
<p>So you may be asking why exactly are Netlify serving up these headers that are very aggressive in terms of anti-caching? Well Netlify has a superb build process that integrates incredibly well with lots of different static build tools. Honestly, if you haven’t tried it you really should! Often you can point Netlify at a Github repository and 2-3 mins later you’ll have a fully hosted, working, static site you can share with friends and family. One of the huge advantages to this is that you can commit a change to Github and minutes later it is reflected on the live site. No more FTPing changes up to production! Or in other words, Continuous Delivery / Continuous Deployment (CD). For this to work you need to ensure that a developer (or user) has the latest assets from the server. There’s no point deploying a change to a site if the browser continues to use an already cached version in a users browser!</p>
<p><strong>Note</strong>: There are ways around this by using something called <a href="https://guides.rubyonrails.org/asset_pipeline.html#what-is-fingerprinting-and-why-should-i-care-questionmark">asset fingerprinting</a>. What this does is generate a unique filename based on the contents of the file (usually a MD5 or SHA hash). So for example <code class="language-plaintext highlighter-rouge">styles-2f3f4bb27f.css</code>. Now whenever this file changes, even by a single byte, the filename will also change, thus creating a new URL and invalidating the cache. I’ve done this for the CSS and JavaScript on this blog which you can see if you view your DevTools network panel.</p>
<p>Now, Netlify has no idea if you are fingerprinting your assets, so it can’t assume you are. Instead they make sure a user gets the latest assets by defaulting to the <code class="language-plaintext highlighter-rouge">Cache-Control</code> headers above. But remember you can override them if you wish using the <a href="https://docs.netlify.com/routing/headers/">header options</a> mentioned earlier.</p>
<p>So what did I do to solve the issue? Well I created a simple <code class="language-plaintext highlighter-rouge">netlify.toml</code> file and placed it in the root of my project:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># netlify.toml
</span>
<span class="c1"># remove `immutable` if you don’t have file fingerprinting available
</span><span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.css"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=31536000, immutable"</span>
<span class="n">Vary</span> <span class="o">=</span> <span class="s">"Accept-Encoding"</span>
<span class="c1"># remove `immutable` if you don’t have file fingerprinting available
</span><span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.js"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=31536000, immutable"</span>
<span class="n">Vary</span> <span class="o">=</span> <span class="s">"Accept-Encoding"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.ico"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=31536000"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.json"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=31536000"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.png"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.jpg"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.jpeg"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.svg"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.avif"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
<span class="p">[[</span><span class="n">headers</span><span class="p">]]</span>
<span class="k">for</span> <span class="o">=</span> <span class="s">"*.webp"</span>
<span class="p">[</span><span class="n">headers</span><span class="p">.</span><span class="n">values</span><span class="p">]</span>
<span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"public, max-age=2419200, must-revalidate, stale-while-revalidate=86400"</span>
</code></pre></div></div>
<p>There’s nothing particularly clever about this file. I’m basically pulling out particular file extensions and changing their <code class="language-plaintext highlighter-rouge">Cache-Control</code> header values. I’m sure there are ways to simplify this file, if there are please do <a href="https://twitter.com/TheRealNooshu">let me know!</a>. <strong>Note</strong>: You may notice I’ve used <code class="language-plaintext highlighter-rouge">immutable</code> on my CSS and JS files. This is because I have fingerprinting in action on these files. I’d recommend removing <code class="language-plaintext highlighter-rouge">immutable</code> if you don’t.</p>
<p>Let’s see what this change looks like in the network panel on our temporary Netlify domain:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/modified-origin-headers.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/modified-origin-headers.avif 1946w" type="image/avif" />
<img loading="lazy" width="1946" height="558" class="figure__image" src="/images/netlify-cloudflare-migration/modified-origin-headers.png" alt="Headers from the origin once modified." />
</picture>
</a>
</figure>
<p>Well the Netlify origin headers are certainly looking a lot better! You can see the headers we’ve set in the <code class="language-plaintext highlighter-rouge">netlify.toml</code> file are reflected in the responses. Let’s see now how this translates when we run it through Cloudflare:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/chrome-cloudflare-cache-status.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/chrome-cloudflare-cache-status.avif 1954w" type="image/avif" />
<img loading="lazy" width="1954" height="420" class="figure__image" src="/images/netlify-cloudflare-migration/chrome-cloudflare-cache-status.png" alt="Status of cache headers once run through Cloudflare to examine the caching status." />
</picture>
</a>
</figure>
<p>Here I’m using Chrome and exposing the <code class="language-plaintext highlighter-rouge">cf-cache-status</code> header for debugging as mentioned above. As you can see it’s looking a lot healthier! Here we now have 6 assets with a status of <code class="language-plaintext highlighter-rouge">HIT</code> and 2 with a status of <code class="language-plaintext highlighter-rouge">DYNAMIC</code>. So most of our page assets now reside in the CDN cache.</p>
<p>We’re getting there, but what are those <code class="language-plaintext highlighter-rouge">DYNAMIC</code> cf-cache-status’s all about? Well Cloudflare still thinks these assets are changing a lot, so they are never cached. The CDN will always go back to the origin for these assets, which is not what we want for a static site. So how do we fix this issue?</p>
<p><strong>Page Rules</strong></p>
<p>This is where the awesomeness of Cloudflare and their free plan delivers again. They have functionality called <a href="https://www.cloudflare.com/en-gb/features-page-rules/">Page Rules</a> available to us. Page Rules give you fine-grained control over how Cloudflare handles certain URLs (or URL patterns). Luckily for us, they give you 3 rules on the free plan. More than what we actually need, we’ll only need 1!</p>
<p>With the page rules we want to tell Cloudflare to ‘Cache Everything’. Basically everything served from our origin is cacheable. We also want to tweak the <a href="https://blog.cloudflare.com/edge-cache-expire-ttl-easiest-way-to-override/">Edge Cache Expire TTL</a>. This is the easiest way to override any existing headers coming from our origin. We are going to do this for the whole domain, so I set <code class="language-plaintext highlighter-rouge">nooshu.com/*</code> for the URL pattern:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/page-rule-pattern.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/page-rule-pattern.avif 1588w" type="image/avif" />
<img loading="lazy" width="1588" height="658" class="figure__image" src="/images/netlify-cloudflare-migration/page-rule-pattern.png" alt="Edit the page rules dialog in the Cloudflare admin panel." />
</picture>
</a>
</figure>
<p>Let’s now see what our CDN caching looks like with the above page rules set:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/with-page-rules.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/with-page-rules.avif 1960w" type="image/avif" />
<img loading="lazy" width="1960" height="410" class="figure__image" src="/images/netlify-cloudflare-migration/with-page-rules.png" alt="Looking at the cache hit rate once the page rules are enabled." />
</picture>
</a>
</figure>
<p>Well look at that! 8 out of 8 assets with a <code class="language-plaintext highlighter-rouge">cf-cache-status</code> of <code class="language-plaintext highlighter-rouge">HIT</code>! Awesome, we’re looking a lot better!</p>
<p><strong>Note</strong>: The minimum TTL you can see on the free plan is 2 hours. This means that if you make an update on your origin the CDN won’t check for an updated version of the file for 2 hours, unless you purge the CDN cache. I’m personally fine with this limitation. If you decide to pay for the ‘Pro’ plan this can be reduced to as little as 30 seconds. So it really depends what functionality you need and what you are willing to pay for. You can always do a custom cache purge on a single URL if needed in an emergency, so that’s an option too.</p>
<p>We’re not quite finished yet. Notice how the HTML page has a <code class="language-plaintext highlighter-rouge">must-revalidate</code> value. This value is redundant since our <code class="language-plaintext highlighter-rouge">max-age</code> is now set to 1 year, so let’s remove it. You could either do this via the <code class="language-plaintext highlighter-rouge">netlify.toml</code> or our Worker code. I personally prefer to do it at the Edge so have done it in the Worker. Simply add the following in the Worker <code class="language-plaintext highlighter-rouge">securityHeaders</code> object above:</p>
<p><code class="language-plaintext highlighter-rouge">"Cache-Control": "public, max-age=60",</code></p>
<p>I’d also recommend renaming the object to something new, since it no longer only contains security headers. Adding this line overrides the <code class="language-plaintext highlighter-rouge">must-revalidate</code> value sent out in the response from Netlify.</p>
<p>I’m not the only person to write about this problem, <a href="https://twitter.com/vladiliescu">Vlad Iliescu</a> has also written about it <a href="https://vladiliescu.net/caching-with-cloudflare-and-netlify/">here</a>, if you want a more detailed explanation.</p>
<p><strong>Bonus: Link Preload Header`</strong></p>
<p>This part is completely optional, and totally depends on your domain and use case. Using a Worker it is dead simple to add and test the <a href="https://www.w3.org/TR/preload/#example-2-using-http-header">Link Preload HTTP Header</a>. This is a header that is sent along with the HTML file that tells the browser to immediately request certain assets you list in the header value. This can be useful for images and fonts for example. Assets that are listed in CSS files have to wait for the CSS to be downloaded and parsed by the browser, before a request can be made to the server to download them. By preloading the asset, it can be cached by the browser and used immediately when needed.</p>
<p>Be warned though, use the preload functionality <a href="https://twitter.com/TheRealNooshu/status/1431905187124371461">sparingly</a>, and be sure to test that it actually <em>improves</em> the performance of your website! Read all about the puzzle that is preloading assets in <a href="https://andydavies.me/blog/2019/02/12/preloading-fonts-and-the-puzzle-of-priorities/">this blog post</a> by <a href="https://twitter.com/andydavies">Andy Davies</a>. Here it is in action <a href="https://twitter.com/TheRealNooshu/status/1433155825724502021">on this blog</a>. This is the single line I added to the Worker:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">Link</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2"></images/image1.png>; rel=preload; as=image; nopush, </images/image2.svg>; rel=preload; as=image; nopush, </images/image3.svg>; rel=preload; as=image; nopush</span><span class="dl">"</span>
</code></pre></div></div>
<p>So what’s the result of all this tweaking on our cache percentage in the Cloudflare control panel? Well it looks a lot healthier:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/post-tweak-cache-graph.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/post-tweak-cache-graph.avif 1540w" type="image/avif" />
<img loading="lazy" width="1540" height="216" class="figure__image" src="/images/netlify-cloudflare-migration/post-tweak-cache-graph.png" alt="Cloudflare overview graph once the tweaks are made hits around 87%." />
</picture>
</a>
</figure>
<p>As you can see from the image above, we’ve gone from a truly terrible 1.98% cache, all the way up to 87.35% cached, and this is growing hour by hour! Not bad considering how little work we’ve actually had to do.</p>
<h3 id="first-byte">First Byte</h3>
<p>The last point to cover is the First Byte score which was a respectable ‘B’. For it to be bumped up to an ‘A’ we need the TTFB to be around 1 second (1004 ms). Thankfully we’ve already done all the work to reduce this TTFB time. Our request no longer goes all the way back to the origin (Netlify). Instead it is cached by the CDN and is delivered to our user from their closest geographic PoP. When it comes to latency the <a href="https://hpbn.co/primer-on-latency-and-bandwidth/#speed-of-light-and-propagation-latency">speed of light is the limit</a>, so the closer the server is to the user, the better!</p>
<p>So let’s take a look at the final score from WebPageTest with all the above tweaks applied:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/after-score-result.png">
<picture>
<img loading="lazy" width="2196" height="422" class="figure__image" src="/images/netlify-cloudflare-migration/after-score-result.png" alt="WebPageTest score once all the tweaks are made looks much better. All green across the board!" />
</picture>
</a>
</figure>
<p>That’s a lot of green! Considering this is on a low spec device on a 3G Fast connection, more powerful devices on faster connections are going to get an even better experience. Here are some raw stats from <a href="https://www.webpagetest.org/video/compare.php?tests=210831_AiDcQR_1c8c05bd7a25ec9c714a81bc31a84296-l:Before%20Tweaks,210903_BiDc28_329d0efa3e272281815e22d165f94fb3-l:After%20Tweaks&medianMetric=SpeedIndex&bg=000000&text=ffffff">comparing my two sets of test runs</a>:</p>
<ul>
<li>16.4% reduction in visual complete time</li>
<li>25% reduction in TTFB</li>
<li>22.3% reduction in load time</li>
<li>16.8% reduction in LCP</li>
</ul>
<p>And a comparison filmstrip:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/compare-filmstrip.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/compare-filmstrip.avif 1822w" type="image/avif" />
<img loading="lazy" width="1822" height="1200" class="figure__image" src="/images/netlify-cloudflare-migration/compare-filmstrip.png" alt="Filmstrip from WebPageTest comparing before and after results." />
</picture>
</a>
</figure>
<p>And no web performance dick measuring contest would be complete without a Lighthouse report including animated fireworks:</p>
<figure class="figure">
<a href="/images/netlify-cloudflare-migration/lighthouse-yay.png">
<picture>
<source srcset="/images/netlify-cloudflare-migration/lighthouse-yay.avif 1074w" type="image/avif" />
<img loading="lazy" width="1074" height="496" class="figure__image" src="/images/netlify-cloudflare-migration/lighthouse-yay.png" alt="Lighthouse score after the test gives 100 across all scores. Fireworks are also visible on the image." />
</picture>
</a>
</figure>
<h2 id="summary">Summary</h2>
<p>Well considering this was only supposed to be a “quick blog post”, it’s turned into a bit of a beast! There’s a lot in the post, so let’s recap what we’ve covered and what infusing Netlify & Cloudflare brings to the table for a static sites like mine:</p>
<ul>
<li>HTTP/3 + QUIC</li>
<li>0-RTT</li>
<li>Cloudflare Workers</li>
<li>Preload Link HTTP Header</li>
<li>DNSSEC</li>
<li>Excellent cacheability of static assets</li>
<li>SEO consideration moving from Github pages to custom domain</li>
<li>Netlify’s amazing Continuous Delivery pipeline</li>
<li>Netlify Forms integration</li>
<li>Easy email integration via ImprovMX</li>
<li>Free hosting on Netlify</li>
<li>Free CDN usage via Cloudflare</li>
<li>Page Rules for fine-grained cache control</li>
<li>Improved security</li>
<li>Improved web performance</li>
</ul>
<p>It’s staggering the amount of functionality and features that are available (for free!) on the web nowadays.</p>
<p>Hopefully you’ve found this post interesting and useful! As always, feedback and improvements are always welcome. Just <a href="https://twitter.com/TheRealNooshu/">let me know via Twitter</a>.</p>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>06/09/21: Initial blog post published.</li>
<li>06/09/21: Added missing aknowlegements for <a href="https://twitter.com/anthony_ricaud">Anthony Ricaud</a> and <a href="https://twitter.com/AndyDavies">Andy Davies</a> for pointing me in the right direction on how to solve the Cloudflare caching issue!</li>
<li>06/09/21: Thanks again to <a href="https://twitter.com/anthony_ricaud">Anthony Ricaud</a> for spotting a number of errors in the post. This is why you let folks proof-read posts first!</li>
<li>23/11/21: Updated the Worker code to remove the added headers and use the new ‘HTTP response headers with Transform Rules’ from Cloudflare.</li>
<li>09/12/21: Updated the post to include the correct expression allowing you to <em>only</em> target the HTML response with the Response Transform rules. Thanks to <a href="https://twitter.com/rboulton">Richard Boulton</a> for his help fixing this issue.</li>
</ul>Matt HobbsI've finally had time to clean up some tech debt, so I migrated this blog onto Netlify fronted by Cloudflare and fixed the web performance issues I spotted.We’ve spotted something on your scan…2021-05-12T12:09:00+00:002021-05-12T12:09:00+00:00https://nooshu.com/blog/2021/05/12/weve-spotted-something-on-your-scan<p>So a word of warning, this isn’t a blog post about web performance. I’m afraid those posts may need to be paused for the moment.</p>
<p>If you’ve already read the original post you can jump straight to the <a href="#postBottom">latest update here</a>, or all updates <a href="#updates">here</a>.</p>
<hr />
<p><strong>Note</strong>: I feel the need to point this out. This post is about a brain tumor and cancer. It’s important to realise that the doctors considerations, and ultimately treatment, are unique to my situation. Anything mentioned in this post shouldn’t be taken as gospel. If a reader finds themselves in a similar situation, please do take the advice of your own specialists. What works (or doesn’t work) for me <strong>will</strong> 100% be different for other people.</p>
<hr />
<h2 id="diagnosis">Diagnosis</h2>
<p>To be fully upfront about all this, on Wednesday 21st April 2021 I was diagnosed with a brain tumor. Completely out of the blue. In the morning I was busy working away still trying to get <a href="https://twitter.com/TheRealNooshu/status/1369343971303055367">Brotli</a> compression tested and enabled on <a href="https://www.gov.uk">GOV.UK</a>, 7 hours later I was given the news that I had a large brain tumor on my right frontal lobe. Hell of a day, huh!</p>
<p>So you may be asking: “Why are you writing about this?”. Well, there are 3 main reasons:</p>
<ol>
<li>I want to get this info out of my head (hah!) while I can still remember it.</li>
<li>From the conversations I’ve had with people since this happened, people are fascinated with how it was discovered.</li>
<li>To offer help to others in a similar situation that I (and my family) are currently in.</li>
</ol>
<p>I’ve seemingly learned a fair amount about myself and the topic in the past 3 weeks. If you’d like to know more feel free to read on, or if you’d prefer to stick to web performance I have plenty of <a href="/tags/webperf/">blog posts here</a> which are much more light hearted!</p>
<p><strong>Note</strong>: We have now <a href="https://twitter.com/TheRealNooshu/status/1390377138860265474">enabled Brotli on GOV.UK</a>, thanks to <a href="https://twitter.com/jaffathecake/status/1392732352795848709">Jake Archibald</a> for the humorous reminder :)</p>
<h2 id="what-happened">What happened?</h2>
<p>This is a question I’ve been asked a lot. From nurses, doctors, pharmacists, out of hours doctors, and family and friends. So I’ll give you a rundown of what happened below:</p>
<h3 id="initial-signs-something-wasnt-right">Initial signs something “wasn’t right”</h3>
<p>Back in October 2020 I started to develop issues with my lower left back and left leg. Multiple consultations with the doctors were of the opinion it was <a href="https://www.nhs.uk/conditions/sciatica/">sciatica</a>. I was given strong painkillers, told to look at getting physio, and make adjustments to my working environment. This actually all made sense at the time. Working from home for 8 months, with less physical exercise, and generally working more hours than usual, all could have easily caused this issue. Unfortunately, no matter what I tried it didn’t solve the issue. I tried: a standing desk, new office chair, new shoes, lots more walks, physio. Nothing seemed to be helping. It got so bad at one point I was crawling on my hands and knees at 3am to go to the bathroom because I was in so much pain. Not exactly what you expect to be doing at the age of 39!</p>
<p>After several months the back and leg pain seemed to fade, but it then turned into extreme heel pain in my left foot, which the doctor said was likely to be <a href="https://www.nhs.uk/conditions/plantar-fasciitis/">plantar fasciitis</a>. Apparently runners tend to get this a lot, but I certainly wasn’t running! So yes, a fun 6 months!</p>
<p>But things have developed further and over the past 4 - 6 weeks I have experienced 3 seemingly very minor seizures. Throughout each I was fully conscious. I will try to describe each of them as I remember them the best I can.</p>
<h3 id="seizure-1-standing-at-my-desk">Seizure 1: Standing at my desk</h3>
<p>I now have a desk that allows me to both <a href="https://www.posturite.co.uk/product/oploft">sit and stand very easily</a>. I’ve got into the habit of standing for meetings with people while on a video call, but when it comes to “real work”, I always sit. This tends to give me a good balance of both sitting and standing throughout the day.</p>
<p>For the meeting in question I just so happened to be on mute and wasn’t speaking at the time. I suddenly felt quite dizzy and had to lean on my desk a little. At first I thought I was having some sort of panic attack. I don’t remember the topic of discussion for the meeting, but at the time it happened I was thinking “maybe this is making me anxious?”. I do remember having a really strange out-of-body experience (OBE) while it was happening. It’s hard to describe, but it’s almost like coming to the realisation that that “you are fully in control of everything you are doing” and “you have an effect on your surroundings and the people you interact with”. It feels more like a powerful emotion than anything physical. Or another way I’ve described it is imagine someone turned off the auto-pilot in your head. You are now having to consciously breathe and blink for yourself, as well as think about all the other things going on too.</p>
<p>Within 10 - 12 seconds this intense feeling had passed and I suddenly felt fine again. It was the first seizure I’d had, so I thought nothing of it, and simply moved on with the rest of the day.</p>
<h3 id="seizure-2-driving-in-the-car">Seizure 2: Driving in the car</h3>
<p>This was around 2 weeks later. Now this one scares me now that I know more! Technically I wasn’t moving at the time, I was parked at traffic lights waiting for the light to turn green so I could then pull across a carriageway into the local supermarket.</p>
<p>Suddenly, I had the same OBE I’d felt before, where I felt like someone had pulled “me” out of the back of my own head. In front of me the lights turned green and I managed to pull forwards and start moving a little, so I had some control of my feet. Unfortunately I didn’t have the strength in my left arm to fully disengage the handbrake, so the warning alarm sounded in the car to tell me it was still engaged. I was fully conscious. My brain was telling my hand to push down and release the handbrake, but my hand simply couldn’t. It was a very strange experience where your arm and hand seemingly has a mind of its own.</p>
<p>Because the alarm was beeping, my wife looked over and mentioned that the handbrake wasn’t down, but she didn’t notice anything else in me that looked different. I didn’t try to speak to her, and I’m not even sure if I’d have been able too if I’d tried. Again, within 10 - 12 seconds this had all passed and I was fully able to move and control the car. We went to the shops and returned home with no further issues. Once again I put it down to a panic attack, but as it all happened so quickly I dismissed it and carried on with life. At this point I hadn’t linked the 2 separate events together.</p>
<h3 id="seizure-3-sitting-at-my-desk">Seizure 3: Sitting at my desk</h3>
<p>This is the one that happened on the 21st April, the day I was diagnosed. It was around 10:50am and I was sitting at my desk working on a document. Suddenly the OBE I’d felt before came back. Only this time I lost control of both lower arms. I was unable to raise them to the keyboard to continue typing. It then developed further and I felt like I’d lost control of the lower half of my face. Almost like someone was gently pulling down on both sides of my cheeks and I was unable to stop them.</p>
<p>I realised this was happening, as I was fully conscious. During this time I must have tried to move, as I’d started to lean to the right side of my chair. I was rotating slowly, unable to really control my movements. Again as with the other seizures, within 10 - 12 seconds this all passed and I could again control what I was doing. But this time it was different.</p>
<p>With this seizure I’d been sitting at my desk, nothing strenuous was happening at all. It was quiet and I was calm. Because of this I was able to notice that I simply couldn’t control my hands and that my face had “drooped”. This immediately alerted me into thinking that I was having a mini-stroke. Now, I’ve seen the <a href="https://www.youtube.com/watch?v=CO2k2Tk23G0">FAST campaign adverts on TV</a>, and realised a stroke can become very serious if not acted on quickly, so that really freaked me out!</p>
<h2 id="nhs-111">NHS 111</h2>
<p>I immediately opened the <a href="https://111.nhs.uk/">NHS 111 website</a> (the UK non-emergency advice service), to fill in the details of what had just happened.</p>
<p>Right on the front page of this service it says:</p>
<blockquote>
<p>Check it’s not an emergency - Call 999 now if you have:
signs of a stroke - face dropping on one side, can’t hold both arms up, difficulty speaking
seizure (fit) - someone is shaking or jerking because of a fit</p>
</blockquote>
<p>At this point I was thinking, “okay.. I’m going to have to phone 999” (for the first time ever in my life). But before I did I thought I’d best discuss it with my wife downstairs. I walked downstairs and calmly told her about what had just happened and also the other brief episodes I’d had weeks before (as I hadn’t mentioned them to her). We both agreed that 999 would be the best option.</p>
<p>It was a strange conversation with the 999 dispatcher. They were talking to me, “the patient”, asking if I was fully conscious and able to lift my arms, move my face etc. These are all standard checks for a stroke. I was able to do everything they were asking me to do with ease. At this point I felt like a total fraud, and I was simply wasting their time. I further described what had happened and that it had now passed, but I still felt a little weak in my lower arms.</p>
<p>I was categorised as a priority 2 (I believe) and told an ambulance would be with us within the next 5 minutes. So I quickly started to pack a few things just incase I needed to leave with them for the hospital sharpish.</p>
<h3 id="ambulance">Ambulance</h3>
<p>Within 5 minutes there was an ambulance outside the door. Two very nice paramedics came in the house and questioned me about how I was feeling and what had happened. They checked my blood pressure, checked my blood oxygenation level, asked me to focus on their fingers, push & pull with my arms against theirs etc. These are all standard checks for suspected stroke patients. It turns out they couldn’t find any issues, which was embarrassing. It got me thinking: “had I just imagined this all happening?” and “am I just overacting and wasting their time?”.</p>
<p>One of the paramedics decided to speak to the specialist unit at the hospital to ask for advice, as they were unsure what would be best (and so was I!). After a 5-10 minute discussion I had 2 options:</p>
<ol>
<li>the paramedics would leave and I’d take this up with my doctor to see what was happening. This could take weeks.</li>
<li>come with them in the ambulance and go to hospital for additional checks.</li>
</ol>
<p>As I was still thinking this could be early signs of a stroke, I (thankfully!) decided that going to the hospital was the best course of action. Even if just to rule out the possibility of a stroke! So that’s what I did. I got in the back of the ambulance and 20 minutes later I arrived at the hospital and was admitted to a very busy ward.</p>
<h2 id="hospital">Hospital</h2>
<p>I’m afraid not much really happened at this point for an hour or so. I had chats with nurses and the doctors on duty, describing what had happened as I mentioned above. The doctor looking after me was a little perplexed by it all, as it was very unusual. I can’t remember the exact term he used, but I believe he was thinking these symptoms sounded like some sort of dissociative disorder. This was because of the OBE I’d described to him.</p>
<h3 id="computerized-tomography-ct-scan">Computerized Tomography (CT) scan</h3>
<p>As part of the standard checks I was given a CT scan. Not only that, they injected me with a special dye called ‘contrast material’. One of the side effects of this contrast is you feel like you’ve wet yourself about 20 seconds after they inject you with it. I honestly thought I’d had an accident when it happened. Apparently it’s related to the kidneys flushing the dye through your system. Either way, it feels weird!</p>
<p>Due to the ward being extremely busy I needed to wait for the results to be examined. This probably took around 5 hours. The first four hours seemed to go quite quickly. I had my noise cancelling headphones and a good book I was enjoying, so I was entertained (although slightly uncomfortable in the high back chairs and a face mask for COVID).</p>
<h2 id="weve-spotted-something">We’ve spotted something</h2>
<p>About 5 minutes before being shown into a private room, the doctor looking after me came up to me and said the sentence:</p>
<blockquote>
<p>We’ve not forgotten about you, we’ve just spotted something on your scan</p>
</blockquote>
<p>Strangely enough, I couldn’t concentrate on my book or music anymore. That was a long 5 minute wait! Well, the 5 minutes passed and the doctor came back over and asked me to follow him into one of the rooms. “Fine”, I thought, “how bad could it be?”. But a few seconds later I could immediately tell something wasn’t right about his body language and manner. He seemed to be struggling to find the right words. And then he came out with it:</p>
<blockquote>
<p>I’m really sorry to tell you this, but we’ve found a brain tumor on your right frontal lobe</p>
</blockquote>
<p>I have to admit the rest of the conversation is mostly lost due to the enormity of what I’d just been told. It felt like the ground dropped from beneath me. But I do remember him saying it “was large”, and that “if you were to wish to have a brain tumor, this would be the type you would want to have”. A strange statement now I think about it, but one that has been confirmed since.</p>
<p>After about 3 or 4 minutes of chatting about who knows what, I just wanted to get out of the room. I actually felt quite sorry for the doctor. He must have done this so many times with patients over the years. I can’t imagine it ever gets any easier. I have a huge respect for anyone who has to do this on a daily basis, it must be totally crushing at times.</p>
<p>I quickly managed to find the words to thank him for letting me know and promptly left the room to try and work out how the hell I was going to tell my wife this over the phone. We’d been in constant communication all day via messenger, since due to the COVID restrictions she wasn’t allowed to stay with me. I knew she would be worried sick at home. It was around 7pm and she’d be getting the kids ready for bed.</p>
<p>I finally mustered up the courage to phone her and tell her what had been said. Again, I don’t really remember the conversation at all. But an hour or so later she’d managed to arrange childcare and she was on her way over to pick me up. She arrived and I think we were both still in shock. We didn’t really say much, other than repeating exactly what I remembered from the doctors conversation. That evening I was sent away with a concoction of drugs to start taking immediately, and then told to return to the hospital tomorrow morning 9am for an <a href="https://www.nhs.uk/conditions/mri-scan/">Magnetic resonance imaging (MRI) scan</a>.</p>
<h3 id="so-what-is-it">So what is it?</h3>
<p>See, now that’s the million dollar (pound) question isn’t it. Not all tumors are the same. Some are benign, some are fast growing, slow growing, large, small etc. The CT scan could only show <em>something</em> was there, but not in the detail needed for a more conclusive result of exactly <em>what</em>.</p>
<p>At this point I’m going to skip ahead. I had the MRI scan the next day. Then a few days later another ‘wet yourself’ inducing CT scan. The full body CT scan was clean, showing that there were no other tumors or areas of concern in my body. Great news!</p>
<h2 id="meet-my-tumor">Meet my tumor</h2>
<p>Time to cut to the chase. This is my brain tumor. There are many like it, but this one is mine. I’ve nicknamed him Gary Glia. Gary because it is a <a href="https://names.darkgreener.com/#gary">seemingly unpopular name nowadays in the UK</a> (can’t imagine why…), so it seemed quite appropriate. And <a href="https://en.wikipedia.org/wiki/Glia">Glia</a> because it is the type of cell in my brain that has gone tumorous.</p>
<p>Gary is an unpaying tenant that has taken up residence in my head. What a prick!</p>
<figure class="figure">
<a href="/images/bugger/top.png">
<picture>
<source srcset="/images/bugger/top.avif 1191w" type="image/avif" />
<source srcset="/images/bugger/top.jxl 1191w" type="image/jxl" />
<source srcset="/images/bugger/top.webp 1191w" type="image/webp" />
<img loading="lazy" width="1191" height="663" class="figure__image" src="/images/bugger/top.png" alt="Top view of the tumor." />
</picture>
</a>
<figcaption class="figure__caption">Gary from the top.</figcaption>
</figure>
<figure class="figure">
<a href="/images/bugger/side.png">
<picture>
<source srcset="/images/bugger/side.avif 921w" type="image/avif" />
<source srcset="/images/bugger/side.jxl 921w" type="image/jxl" />
<source srcset="/images/bugger/side.webp 921w" type="image/webp" />
<img loading="lazy" width="921" height="525" class="figure__image" src="/images/bugger/side.png" alt="Side view of the tumor." />
</picture>
</a>
<figcaption class="figure__caption">Gary from the side.</figcaption>
</figure>
<figure class="figure">
<a href="/images/bugger/back.png">
<picture>
<source srcset="/images/bugger/back.avif 912w" type="image/avif" />
<source srcset="/images/bugger/back.jxl 912w" type="image/jxl" />
<source srcset="/images/bugger/back.webp 912w" type="image/webp" />
<img loading="lazy" width="912" height="513" class="figure__image" src="/images/bugger/back.png" alt="Back view of the tumor." />
</picture>
</a>
<figcaption class="figure__caption">Gary from the back.</figcaption>
</figure>
<p>A few days later I had a meeting with the neurosurgeon where I was shown the above images. Now full respect to the neurosurgeon I spoke too, I like him. He was straight down the line and straight to the point. No sugar coating, no forewarning about the size of the tumor. After 30 minutes of chatting about all of the above, he simply pulled his phone out and showed me image number 1 you see above. You could say I wasn’t exactly prepared for the image, a understatement if ever there was one. But in all honesty I don’t think I ever would be. So it was a good tactic. Make it quick and painless, just like ripping off a band-aid!</p>
<p>So, what do we know about Gary?</p>
<ul>
<li>He’s big… look at the size of this unit. I’m amazed I can even function at all to be honest!</li>
<li>He’s of the type called <a href="https://www.macmillan.org.uk/cancer-information-and-support/brain-tumour/oligodendroglioma">oligodendroglioma</a> and currently believed to be a Grade 2 (slow growing) tumor. Wondering how you pronounce that? Isn’t <a href="https://www.youtube.com/watch?v=eYYMPR14PLI">YouTube great</a>!</li>
<li>He’s very rare, with <a href="https://www.cancer.gov/rare-brain-spine-tumor/tumors/oligodendroglioma">only 1,217 people diagnosed with this type in the US every year</a> out of a population of 328 million!</li>
<li>He’s a number of years old. The neurosurgeon said a couple of years, but I personally suspect he could be 7 or 8 years old due to other symptoms in my past I now think could be related.</li>
<li>He’s a prick.</li>
</ul>
<p>So I’ve basically won a really shit version of the lottery… but then again maybe not. It could be so, <a href="https://en.wikipedia.org/wiki/Glioblastoma">so much worse</a>! I actually have options that I go into in the next section. There are people out there who are given this devastating news that include words like: ‘Stage 4’, ‘aggressive’, ‘inoperable location’ and are literally given months to live, and there’s nothing that can be done. Thankfully, I’m not in that place!</p>
<h3 id="the-positives">The positives</h3>
<p>I don’t want to depress everyone, so let’s focus on some of the positives I’ve since found out:</p>
<ul>
<li>It’s believed to be a slow growing tumor, that is currently classed as the less aggressive type (oligodendroglioma rather than anaplastic oligodendriogliomas).</li>
<li>It originated and is primarily located in my brain. I have no other areas in my body showing tumors.</li>
<li>It responds really well to chemotherapy. If I turn out to be <a href="https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma">1p/19q Codeleted</a> then chemotherapy is even better.</li>
<li>My symptoms (seizures) can be managed via drugs, which now look to have minimal side effects.</li>
</ul>
<h2 id="what-happens-next">What happens next?</h2>
<p>I’ve had a really great set of meetings with some very knowledgeable folks from Oxford University and the John Radcliffe hospital in Oxford. There are three options available to me:</p>
<h3 id="1---do-nothing-and-active-monitoring">1 - Do nothing and active monitoring</h3>
<p>Basically manage the symptoms and look at how the tumor progresses over time. Go for regular checkups and see how long I hold out.</p>
<h3 id="2---biopsy-to-determine-the-exact-type">2 - Biopsy to determine the exact type</h3>
<p>At the moment Grade 2 is suspected because the CT scan hasn’t shown the usual telltale signs of Grade 3 (which is bright white ‘spots’ in the scans). Doing this would require brain surgery to remove a small portion of the tumor to be tested. I’d then need to recover from this, before a more specific treatment plan is created.</p>
<h3 id="3---remove-as-much-as-possible-via-major-surgery">3 - Remove as much as possible via major surgery</h3>
<p>This is the last option, and the one I’ve decided to go for. This is where the surgeon open’s my head and will try to remove as much of the tumor as safely as possible. Once completed, additional surgery may be required to remove any tumor that is left over, or radiotherapy and (or) chemotherapy can be used to finish it off.</p>
<p>This surgery can sometimes take up to 9 hours, and it will most likely involve me being awake for it too (so not under general anaesthetic). I find that part both fascinating and terrifying all at the same time! The reason for this is because they want to be able to ask me questions while they are operating e.g. “move your left hand”, “blink your eyes”, “speak to us”. That way, they can be sure they are minimising the damage to the good parts of my brain. It blows my mind (hah!) that it’s possible to <a href="https://www.bbc.co.uk/news/av/uk-england-london-51557044">even do this</a>, but I’m very glad they can!</p>
<p>But, as with all types of surgery (especially on the brain) there are some inherent risks:</p>
<ul>
<li>10-15% chance that it could cause a stroke or paralysis</li>
<li>less than 5% chance of a seizure occurring during surgery. This can be very bad indeed, and will result in the operation being stopped immediately</li>
<li>small chance of temporary paralysis on my left hand side</li>
<li>small chance of infection due to the head being open for a number of hours</li>
</ul>
<h2 id="decision">Decision</h2>
<p>As mentioned above, looking at the size of Gary, and weighing up the risks, 3 seems like the only viable option to me that could lead to the best overall outcome. Considering I’m 39 and not in my 70’s, compared to the alternative of doing nothing, it feels like a simple decision to make. So that’s what I’m doing. I refuse to <a href="https://poets.org/poem/do-not-go-gentle-good-night">go gentle into that good night</a>, so it’s time to make plans to evict Gary Glia from my head.</p>
<p>It’s also worth remembering: neurosurgeons are amazing people and they do this day in, day out, as their job! This is just a walk in the park for them…</p>
<h3 id="moving-forwards">Moving forwards</h3>
<p>So what happens now? Well I’m now on the waiting list for the surgery, and I have additional monitoring and scans coming up. It’s basically a case of waiting for a date and managing the symptoms. If it is that the seizures come back, or anything else gets worse, then things get reassessed by the specialists and the plan changes. I’m continuing to work as much as possible, and spending time with family. For me it’s all about keeping my thoughts away from the dark places they could so very easily slip into, which I fully admit they did for the first week after diagnosis.</p>
<h2 id="support">Support</h2>
<p>So here’s a section I’ve compiled for readers who may find themselves in the same position as I’m now in. Thankfully, brain tumors are rare, even more so if of the type oligodendroglioma and anaplastic oligodendroglioma, so there may not be many of you out there. But if there are, here are a few support links to help you out:</p>
<ul>
<li><a href="https://www.macmillan.org.uk/cancer-information-and-support/brain-tumour/oligodendroglioma">Macmillan cancer support - Oligodendroglioma</a></li>
<li><a href="https://www.thebraintumourcharity.org/brain-tumour-diagnosis-treatment/types-of-brain-tumour-adult/oligodendroglioma/">The Brain Tumour Charity - Oligodendroglioma</a></li>
<li><a href="https://www.braintumoursupport.co.uk/">Brian Tumor Support Charity</a></li>
<li><a href="http://braintumouraction.org.uk/">Brain Tumour Action Charity</a></li>
<li><a href="https://brainstrust.org.uk/">Brain Trust Charity</a></li>
</ul>
<p>As you can see, there’s lots of support out there. Macmillan and the Brain Tumour Charity have been fantastic over the past few weeks. The work they do for people needs to be talked about more. They really are unsung heros working behind the scenes to make people’s lives better, in some of the most challenging times they will ever face.</p>
<p><strong>Pro tip</strong>: Be <em>very very</em> wary of ‘Googling’ your type of tumour and reading random forums. There’s a lot of scary stats and topics out there that may be totally fictional, or only apply for that specific individuals case! A ‘random Google’ is a dangerous path to take. Trust me I did it and quickly stopped! If you can, stick with <a href="https://pubmed.ncbi.nlm.nih.gov/">approved sources</a> and those recommended by some of the charities listed above. Or speak to the team looking after you for advice.</p>
<h3 id="practical-steps">Practical steps</h3>
<p>Now, I realise there are some really terrible topics to have to talk about in the section below, but unfortunately they need to be considered. I’ve actually found the practical side of dealing with all this really useful. I’m completely powerless in terms of a future date for the surgery and the results of said surgery. But what I <em>can</em> do is make preparations for eventualities that <em>may</em> happen. It may not seem like much, but with the practical side it feels like I’m actually doing <em>something</em> useful. I’m moving the ball ever so slightly forwards.</p>
<p>So here are a few topics you could (should) start to think about:</p>
<p><strong>Admin tasks</strong></p>
<ul>
<li>Make sure you share passwords with your other half! I recommend setting up a <a href="https://1password.com/families/">family 1Password account</a>. With a shared vault you can securely share documents, passwords, notes, all in the cloud. No need for unsecure notebooks with written passwords that could be lost or stolen!</li>
<li>Cancel your driving licence and remove yourself from the car insurance. In the UK you won’t be allowed to drive if you have a brain tumor / seizures.</li>
<li>Make sure your partner is listed on household bills, and can talk to the companies.</li>
<li>Is your paperwork filed away in a single place and ordered logically?
<ul>
<li>Does your partner have easy access to it all?</li>
</ul>
</li>
<li>Do you have a safe? If so, do they know how to get into it? Share the combination / backup key with them.</li>
<li>Start to make a list of what you will need to (eventually) take to the hospital. Maybe even start to pack a bag straight away. It’s not much, but at least it’s there ready to go when needed. Involve your partner in this planning too.</li>
</ul>
<p><strong>Finances</strong></p>
<ul>
<li>Check if your employer offers any pension scheme / death in service benefits. Is your partner listed correctly on these?</li>
<li>Ensure your partner can speak / access bank accounts if required.</li>
<li>Share details with your partner about your life insurance, income protection, mortgage payment protection etc. Maybe even speak to an Independent Financial Adviser (IFAs) about all this to check it is all in order.</li>
<li>Are there any extra benefits you are now eligible for e.g. <a href="https://www.gov.uk/pip">Personal Independence Payment (PIP)</a>? The charities listed above can help you with advice on this.</li>
<li>Consider setting up a joint account with your partner, and have shared money “just in case”.</li>
<li>Create a spreadsheet with information about your household utility providers e.g. who’s your electricity provider. Include account numbers, contact numbers, cost per month, payment method etc
<ul>
<li>On the same sheet you could include details about finances (Credit Cards, loans, higher purchases etc)</li>
<li>Include details about pensions, banks, building societies you are with including account numbers and contact details.</li>
<li>Remember to securely store this information (see the 1Password recommendation above)</li>
</ul>
</li>
<li>If in the UK you will now most likely be eligible for <a href="https://www.nhs.uk/nhs-services/prescriptions-and-pharmacies/who-can-get-free-prescriptions/">free prescriptions on the NHS</a>. You can apply for this through your local doctors surgery. So make sure you do this, as you’ll likely going to need a fair few drugs over the coming months / years!</li>
</ul>
<p><strong>Documents</strong></p>
<ul>
<li>Make sure you have a last will and testament prepared. Make sure executor’s are informed and listed correctly.</li>
<li>Ensure you have your <a href="https://www.moneysavingexpert.com/family/power-of-attorney/">Lasting Power of Attorney</a> in place with your partner listed, as well as backups. You can ‘do it yourself’, or speak to an specialist advisor for this. I opted for an advisor, I don’t have the mental capacity or time at the moment to do it manually.</li>
<li>Make a list of questions to ask nurses / doctors etc. I’ve <a href="https://docs.google.com/document/d/1Hk23n-1vKpxyLLvqekyJfh0stlHYZwkudhnck3XzNOM/edit?usp=sharing">shared the list I made here</a> that you can copy if you are struggling.</li>
</ul>
<p>Above all else remember, you can make plans and be prepared, but even the most well-made plans can go pear-shaped quickly. It simply isn’t possible to pre-think and pre-plan everything for every eventuality. So there’s only so much you <em>can</em> do. But much that is listed above could be considered “good practice” for any adult anyway, even if you aren’t in the same situation I’m in.</p>
<h3 id="plans-for-this-blog-post">Plans for this blog post</h3>
<p>So what are the plans for this blog post? Well, I plan to keep this post updated with my progress for those who are interested. This will be the only blog post where I mention Gary the prick. Basically it will be a self-contained blog post about a topic I never expected to be living, let alone writing about! And as with my web performance blog posts, I’ll try to keep them as ‘living documents’ that are constantly updated as things change. I will continue to use the ‘Post changelog’ below in exactly the same way.</p>
<h2 id="thanks">Thanks</h2>
<p>So I have a huge number of folks to thank who have been amazing over the past few weeks:</p>
<ul>
<li>My wife, who has been so incredibly brave throughout this. Immediately looking for information and support that is out there (both emotional and practical). She amazes me every day.</li>
<li>My parents, sister, brother-in-law and immediate both sides of the family who have rallied around to help us in whatever way they can. At the moment COVID restrictions make this hard, but this should become easier in the UK very soon. I can’t thank them enough.</li>
<li>Line managers from GDS, both past and present, who have helped ease my mind when it comes to the logistics of work in the future.</li>
<li>The Frontend community at GDS. I feel so lucky to be able to head up and work with such an amazing group of talented and dedicated individuals. They inspire me every day.</li>
<li>A certain ex-GDS colleague who shall remain nameless. They have been a fountain of knowledge in so many different areas that, in all honesty, I didn’t even realise existed 3 weeks ago!</li>
<li>And last, but certainly not least, the wonderful paramedics, doctors, nurses, and all workers of the <a href="https://www.nhs.uk/">National Health Service</a>. The quality of care I’ve received so far has been truly outstanding. It’s all the more impressive considering the huge strain COVID is placing on the whole system at the moment. They really are heros!</li>
</ul>
<p>And finally, thank you reader for taking the time to read the post. It wasn’t at all easy to write, but I feel so much better for actually taking the time to do it. Hopefully some of you find it useful. If helps even just a single person get through such a monumental, life changing event such as this, then it was totally worth my time writing it.</p>
<h2 id="updates">Updates</h2>
<h3 id="another-seizure---monday-10th-may">Another seizure - Monday 10th May</h3>
<p>On Monday night I had a really ‘fuzzy’ head. That’s the best way I can describe it. It was almost like I’d taken some sort of sleeping tablet. I also had a ‘prickly’ head and scalp, quite sensitive to touch. I went to bed early but woke up at 2:30am with the same OBE I’d experienced before. I think I had another seizure in my sleep. I can’t be 100% sure as I fell to sleep again right after it. I just remember waking up to the same feeling.</p>
<p>On speaking to the team of specialists they said it’s most likely from lack of sleep, my body adjusting to the drugs and general stress with everything going on at the moment. They aren’t too concerned and my medication will be staying at the same level for the moment. If it gets worse in the coming weeks they will look to up the amount of anti-seizure medication.</p>
<p>I just need to work out what the triggers are, and the warning signs of one occurring. It’s a whole new world for me, so I don’t recognise the signs yet. You can live 39 years in a body, and still have no idea what is going on with it…</p>
<h3 id="messages-of-support---thursday-13th-may">Messages of support - Thursday 13th May</h3>
<p>I’ve been absolutely blown away by the huge number of messages of support I’ve had since writing this post. I <a href="https://twitter.com/TheRealNooshu/status/1392526600047140864">posted it on Twitter</a> and <a href="https://www.linkedin.com/feed/update/urn:li:activity:6799090494472245248/">on LinkedIn</a> and have received literally hundreds of messages. I’ve tried to respond to as many as I can, but I may not have seen them all. So sorry if I haven’t replied! Just to say thank you to everyone who has reached out. I really do appreciate the support. It’s all been quite overwhelming, and will admit to welling up a few times from the sheer number of them. What a crazy couple of days.</p>
<h3 id="neurosurgery-pre-assessment-clinic---saturday-15th-may">Neurosurgery pre-assessment clinic - Saturday 15th May</h3>
<p>On the 20th May I have another appointment at the hospital. This is the pre-assessment for the surgery. I’m not 100% sure what is involved, but I’m going there with a list of questions I’m hoping to get answered. On speaking to a senior member of the team during the week the timeline for surgery has been revised down from 3 months to 4-6 weeks (assuming other higher priority cases don’t bump me down the list). This is good news, but also makes me a little anxious! I was mentally preparing myself for having 3 months to get used to the idea of awake open-head surgery. 4-6 weeks seems way too soon! But it is what it is. I’ll get used to it, it’s not like I really have much of an option…</p>
<h3 id="yet-another-seizure---wednesday-19th-may">Yet another seizure - Wednesday 19th May</h3>
<p>A short update. I had a very short ‘mini’ seizure in the morning just after waking up, around 6:10am. I was feeling perfectly fine, then suddenly my face felt quite warm. I could feel something was about to happen. I had the feeling of losing control of my face again (drooping), then started rocking gently backwards and forwards in my seat as has happened before. I was fully aware and conscious during this time. Around 10 seconds later I was back in control. It was nowhere near as intense as some of the other seizures I’ve had, but it’s still a seizure. So it’s important to keep the specialists informed. I believe we may look into upping the dosage of the <a href="https://www.nhs.uk/medicines/levetiracetam/">Keppra</a> drug I am on in the coming days.</p>
<h3 id="pre-operative-assessment-appointment---thursday-20th-may">Pre-operative assessment appointment - Thursday 20th May</h3>
<p>So today was an interesting day. I learnt a lot about the upcoming treatment and the weeks ahead. The more information I have at the better, as planning anything is virtually impossible at the moment! I had a 2.5 hour meeting with a friendly senior nurse on the team looking after me. She answered all my questions and gave me a rundown of the coming weeks, months, and years. I’ve tried to break what was said down into sections following a chronological order of future events. Ultimately the reason for the consultation was to prepare me for surgery in the future, which is a good thing. Although I’m still very nervous about the thought of a whole team poking around in my head, the information given to me today has really helped me understand it all.</p>
<p><strong>Future seizures</strong></p>
<p>I need to start logging any previous and future seizures, this is very important. Ultimately the plan is to achieve a sustained period where I don’t have any seizures, while keeping me on the minimal amount of drugs possible (due to drug side effects). There’s a fine balance to achieve this, and we aren’t there yet. I seem to be getting one seizure a week at the moment. I’d say my drugs will be increased in the coming days. I’ve also been asked to try to speak the next time a seizure occurs. The team has questions like: “Am I able to talk at all?”, “Does it cause slurred speech?”, “Are the words I want to say totally different to what comes out of my mouth?”. The team are interested to know how a seizure affects my speech, since this can give an insight into the parts of the brain being affected by the tumor. So that’s on my to-do list for next time!</p>
<p><strong>Neuropsychology assessment</strong></p>
<p>I’m scheduled to have a neuropsychology assessment in the coming weeks. I didn’t realise how important this actually was until today. It’s going to be a long process, possibly a 3 hour+ meeting where members of the team will conduct many memory and cognitive tests on me. Ultimately, this testing will be used as to set a ‘baseline’ for various aspects of my current brain function. This baseline will then be used both <em>during</em> and <em>after</em> the surgery.</p>
<p>It was explained to me that during surgery they will be constantly asking me questions and asking me to move parts of my body. While this is happening they will be manipulating and testing neurons in my brain to see what effect it has on my cognitive ability. Only once the surgeon is 100% happy that said part of the brain is safe to remove, will it actually be removed. As you can imagine this is going to be a long process with a tumor the size of mine! The baseline will also be used to compare brain and memory function during the recovery period. Without this baseline it would be extremely difficult, if not impossible, to measure the effect the surgery has had on me.</p>
<p>Only after this baseline has been established will my surgery be scheduled. That’s how important this assessment is!</p>
<p><strong>Pre-surgery preparation</strong></p>
<p>I’ve been given a set of items to use the night before surgery.</p>
<ul>
<li>Antibacterial body wash</li>
<li>Antibacterial cream (for my nose to combat <a href="https://www.nhs.uk/conditions/mrsa/">MRSA</a>)</li>
<li>Antibacterial mouthwash</li>
</ul>
<p>Basically I need to be as germ free as possible before having surgery. I will be using all three of these the day before and on the morning of the surgery. Body wash all over, leave for 1 minute then rinse. Rub the cream up my nose 3 times throughout the day and in the morning. Mouthwash 3 times the day before, and again in the morning of surgery. Doesn’t sound pleasant, but if it means there’s more chance of success I’m more than happy to do it!</p>
<p><strong>The surgery</strong></p>
<p>Now this is where it all starts to get very interesting, as the process was explained to me in detail so that I know what to expect on the day. I will likely be told on a random Tuesday in the future that I’ve been scheduled for surgery on the following Friday. So I’ll only get 3 days notice! That’s going to be an interesting week! I’ll have a COVID test 2 days before, then on the Thursday I will go into the ward and prepare for surgery the following day.</p>
<p>I will have another MRI scan and a brain mapping scan. The brain mapping scan is used to map the coordinates and direction of travel which the surgeon and team will enter my skull. All this is planned to the smallest detail. Nothing like Dr. Nick off The Simpsons (thankfully!).</p>
<p>The surgery is likely to last 5 hours, 3 of those with me being awake. They will put me to sleep for the first hour to prepare me since it sounds a little unpleasant. I will be attached to a metal frame via a set of small screws that are drilled into my skull. I will then be woken up and won’t be able to move my head. My head will already be open. Upon waking I will be surrounded by a team of people and won’t be able to see or feel anything that is happening behind me. The team will continue to ask me questions and to do things while they operate and slowly remove the tumor (using the methodology mentioned earlier). Once they are happy, they will again put me to sleep for an hour, patch me back up and I will be sent to recovery. It all sounds so simple doesn’t it!</p>
<p><strong>Recovery</strong></p>
<p>Assuming the surgery goes well I will be admitted to the recovery ward for around 24 hours. I’ll have a tube coming from my head to allow fluid from the wound to drain safely away, as well as various other pipes attached to my body. The aim is to get me up and mobile (at least a little) within the first 24 hours. This is to reduce the chance of blood clots, as there’s a raised chance these can happen for up to 3 weeks after surgery. If all goes well and I don’t show signs of seizures, I will be sent home. How crazy is that, major brain surgery to home within 24 hours! If it is that there are complications, I’m unable to move etc, I will be kept in for longer. It’s all a complete unknown at the moment.</p>
<p>Once Gary the prick has been extracted, he will be tested to see what exactly he is (or was), and this data will be used to inform my ongoing treatment plan. Genetic marker testing (e.g for the <a href="https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma">1p/19q Codeleted gene</a>) will also be conducted, and this data will also be used to plan the best treatment moving forwards. The ‘default’ treatment for this type of tumor is surgery, so it’s likely I will have more head screwing fun in the future! Radiotherapy can be used once, and chemotherapy can be used multiple times to clear Gary’s leftovers should it be needed. We shall only know more after the messy uninvited squatter has been forcibly removed.</p>
<p><strong>Back to ‘normal’ and work</strong></p>
<p>I’m likely to need a minimum of 6-8 weeks off work. In this time I will likely exhibit changes in mood, behavior, concentration, and personality. This is all perfectly normal due to the fact that my brain will be adjusting both physically and chemically to the tumor not being there anymore. If I require further surgery and/or treatment like chemotherapy/radiotherapy, I may need a little longer to recover. Again this won’t be known until after surgery.</p>
<p>The key is to take this slowly. I already know this is going to be an incredibly frustrating time for me. I know myself. I’m not one for sitting around doing very little. I’m always on the go and doing <em>something</em>. But I’m going to have to live with it for a few months. Maybe I can start watching all of the box sets & movies I’ve always wanted to watch! Or start tackling the tonne of books I’ve never read over the years. I mean after all, it sounds better than the alternative!</p>
<h3 id="a-bumpy-weekend---saturday-22th---sunday-23rd-may">A bumpy weekend - Saturday 22th - Sunday 23rd May</h3>
<p>So it was a bit of a bumpy weekend in terms of how I’ve been feeling. I’m now on anti-seizure medication that I will be taking for the foreseeable future, maybe even forever depending on how surgery goes.</p>
<p><strong>Saturday</strong></p>
<p>Day went well until about 10:30am, suddenly I felt extremely tired, sat on the sofa and promptly fell to sleep. Woke up 10 mins later to find my 4 year old son had covered me in cushions and was laughing his little head off as I woke up. I was completely and utterly confused as to what had happened. The medication I’m on mentions drowsiness / tiredness, so I put it down to that. I didn’t feel 100% all day, generally quite weak. I had the same feeling around 6:30pm where I could barely lift my arms and just wanted to sleep (this is not at all like me, as I generally hardly sleep!). This feeling passed after an hour or so, after a little food.</p>
<p><strong>Sunday</strong></p>
<p>Went to the park with my wife and children, no issues at all. Around 1pm I had a sudden feeling of weakness. Went to the living room, sat on the sofa, promptly fell to sleep. My 4 year old son is very opportunistic, again covered me in cushions, again in hysterics when I woke up 20 mins later. He’s going to be a fun teenager…</p>
<h3 id="seizure-in-the-kitchen---monday-24th-may">Seizure in the kitchen - Monday 24th May</h3>
<p>Due to everything that happened over the weekend, which we assumed were the side effects of the drugs I contacted the team of specialists looking after me early in the morning via email. I knew because of this they would be in contact with me later in the day.</p>
<p>Around 7:30am I was standing in the kitchen having just prepped the kids breakfast. I suddenly had what I can only describe as an emotionally driven feeling that <em>something</em> was about to happen. I could feel something building. I immediately recognised something wasn’t right and sat down next to my wife at the kitchen table. She could see something wasn’t right either, so stood up next to me and so as to shield me from the children. I suddenly couldn’t really control my movements and started gently rocking backwards and forwards. I was conscious and remembered that I’d been asked to speak the next time this happened. I said out loud “Can you hear me?” to my wife. It sounded perfectly clear in <em>my</em> head, but to my wife it sounded slightly slurred. She could still make out what I was trying to say though. Apparently while this was happening my eyes were very wide, but that could really have just been panic on my part. It all passed so quickly as they always seem to do.</p>
<p>Going back to the emotion I felt. I’m not sure if this was something triggered by my subconscious, as in “bubbling up” to tell me “stuff is going on here, get ready for it”, or if it was me thinking about a seizure and then this triggered it because I thought of it. I’m thinking the former to be honest.</p>
<p>On speaking to the specialist nurse she seemed to think that the episodes that occurred over the weekend weren’t side effects of the drugs. These were actually a different type of seizure that were happening over a longer period of time. Well at least now I know! Because of this my anti-seizure medication has been increased, and will be increased again in a week. The aim is to have a long period of time between each, or even stop them completely before surgery. So we shall see over the next week if this increase actually works.</p>
<h3 id="obtained-my-full-scan-data---tuesday-25th-may">Obtained my full scan data - Tuesday 25th May</h3>
<p>When in one of my follow-up meetings (20th May) with a senior nurse I happened to ask the question if I could have a copy of the original photos from the scan. All I had at the time was a screenshot from a video call that was shown to me off the neurosurgeons smartphone (what a crazy sentence, isn’t modern technology amazing!). I was told to visit the hospitals radiology department, fill out a form at reception and it could all be arranged. On the form the interesting part for me was the following section:</p>
<figure class="figure">
<a href="/images/bugger/scan-application.jpg">
<picture>
<source srcset="/images/bugger/scan-application.avif 1260w" type="image/avif" />
<source srcset="/images/bugger/scan-application.jxl 1260w" type="image/jxl" />
<source srcset="/images/bugger/scan-application.webp 1260w" type="image/webp" />
<img loading="lazy" width="1260" height="626" class="figure__image" src="/images/bugger/scan-application.jpg" alt="Section of the image scan application form where you get to choose how they send the data to you." />
</picture>
</a>
</figure>
<p>The top section allows you to choose <em>how</em> they send you the data. For a patient it was either as an online link or as an encrypted CD. It shows you how old the form is, no mention of USB sticks, or even DVD media! I chose the option where an online link is sent to me via email.</p>
<p>It’s also worth noting the sentence below this box:</p>
<blockquote>
<p>The patient is entitled to a copy of all the Radiology imaging we retain at this trust. However if specific episodes are required, please specify below.</p>
</blockquote>
<p>So depending on your local NHS trust, you should be able to do this yourself (should you be interested).</p>
<p>I received an automated email saying this could take up to 28 days, but 2 days later I had an email from a member of the team saying that my scans were scheduled for the overnight upload and I’d receive the link the following day. Upon receiving the link they sent me a one time password (OTP) via my mobile number that allowed me to login. The web performance of the system was shocking. Took around 20 seconds to load on a 50 Mbit connection and a modern browser! Thank you JavaScript…</p>
<p>Once I clicked on the download button I realised this wasn’t just a set of high resolution images, as it was a zip file for 1.3 GB of data! Intriguing! It took me a number of attempts to actually download the data, and I had close friends try too. Their server connection was slow and unreliable, and it also doesn’t seem to support
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests">HTTP range requests</a>, so it wasn’t possible to resume a download after already managing to grab 880 MB of it! Best thing to do I found was to wait until later in the evening. I managed to get a download speed of around 2.7 MB per second, so it only took around 15 minutes in the end.</p>
<p>The reason the zip file is so huge is because it is sent in a standardized format for use in specialist tools that can interpret the data. Here’s me thinking it would be just a few images… nope! It’s literally all the images from all scans completed so far! Hence the box on the form stating which scans you require.</p>
<figure class="figure">
<a href="/images/bugger/scan-thumbnails.jpg">
<picture>
<source srcset="/images/bugger/scan-thumbnails.avif 1008w" type="image/avif" />
<source srcset="/images/bugger/scan-thumbnails.jxl 1008w" type="image/jxl" />
<source srcset="/images/bugger/scan-thumbnails.webp 1008w" type="image/webp" />
<img loading="lazy" width="1008" height="859" class="figure__image" src="/images/bugger/scan-thumbnails.jpg" alt="Image of lots of thumbnails of my head. All the images captured in the scans!" />
</picture>
</a>
</figure>
<p>I believe there are around 4,000-5,000 images in total from 1 CT and 1 MRI scan! Thankfully there are tools that you can use to view these images. And not just view them, they allow you to navigate through all the scans. Each image is essentially a “slice” of the brain. The tools allow you to move through all 3 axis in near real time, with each of the corresponding panels changing depending on the input from the others.</p>
<figure class="figure">
<a href="/images/bugger/three-axis-navigation.jpg">
<picture>
<source srcset="/images/bugger/three-axis-navigation.avif 1400w" type="image/avif" />
<source srcset="/images/bugger/three-axis-navigation.jxl 1400w" type="image/jxl" />
<source srcset="/images/bugger/three-axis-navigation.webp 1400w" type="image/webp" />
<img loading="lazy" width="1400" height="923" class="figure__image" src="/images/bugger/three-axis-navigation.jpg" alt="Image of the three navigation axis from my MRI scan." />
</picture>
</a>
</figure>
<p>In the above image you can see my MRI scan. It’s essentially an orthogonal view of my head, with each of the panels represents a different axis:</p>
<ul>
<li>Left panel: Top of my head (Axial view)</li>
<li>Top right: Front to back of my head (Coronal view)</li>
<li>Bottom left: Side of my head (Sagittal view)</li>
</ul>
<p>The solid blue line you see in the views is the current plane I have selected to see. As you move this plane, all three images change accordingly. Notice how the blue line on the axial view (left panel) matches that of the coronal view (top right) in terms of position. The resulting image in the sagittal view (bottom left) is the image that corresponds with this selection I have made.</p>
<p>It’s so incredibly clever and absolutely fascinating (to me at least)! I spent the best part of 2-3 hours looking at my own brain scans. I wish I knew what it all meant, but I plan on quizzing the experts when I get the chance. I’d like to write a blog post all about it in the future. So, watch this space!</p>
<p>For those interested in doing the same the tools I used are:</p>
<ul>
<li><a href="https://github.com/nroduit/Weasis/">Weasis</a></li>
<li><a href="https://horosproject.org/">Horos</a></li>
</ul>
<p>Horos takes it a step further, as it allows you to view your scan in 3D, and even <a href="https://twitter.com/TheRealNooshu/status/1397459707422547968">export an animation</a> to show all your friends at the next soirée you attend! So yes, if you ever fancy looking at your own insides in detail, remember you can request your CT and MRI scan data!</p>
<h3 id="cloudy-head-seizures---friday-28th-may">Cloudy head seizures - Friday 28th May</h3>
<p>So I think I’ve started to recognise the signs of having a seizure. But this isn’t an obvious type of seizure that started this whole saga off 5 weeks ago. This looks to be a different type that I touched on in a previous update. In my ‘Seizure spreadsheet log’ I’ve written:</p>
<blockquote>
<p>Weakness in arms, “cloudy” thinking. Feel it around my face, tingling in forehead, almost throbbing sensation. Sleepy. Hard to keep my eyes open. Muscles changing in my face (odd tightening up from the middle). Feeling coming on for a number of hours. Feels like it is building up to a seizure. Brain feels “heavy”. Went to sleep for 40 mins, felt like something ‘lifted’, may have had another seizure in my sleep. Had the same out of body experience while sleeping.</p>
</blockquote>
<p>It’s hard to say, but I think I may have had another more obvious seizure while resting. I remember it happening so I did try to move my arms and they seemed fine. So who knows! Urghhh! But after my ‘sleep’ for 40 minutes or so I felt a whole lot better.</p>
<p>Also, I’ve noticed my left foot (heel) has started to hurt, which is one of my initial symptoms that I was blaming on sciatica. So I’m going to be mentioning that at my next catchup with the team.</p>
<h3 id="date-for-the-memory-test---tuesday-1st-june">Date for the memory test - Tuesday 1st June</h3>
<p>So I’ve now been given a date for the baseline memory test. This is happening on the 29th June, a whole month away. Not ideal, and I’m a little torn with the date. In some ways I want it sooner to “get it out the way”, but in other ways I want it later. At least when it’s later it gives me more time to ‘prepare’. I still feel I have so much general life administration left to complete before the ‘big day’.</p>
<p>The symptoms with my left foot still persist but tend to vary day to day. Because of this I may need to be given different drugs and / or have additional scans to see what Gary is up to. I’d like to think he hasn’t gotten even bigger in the last month, but it wouldn’t surprise me!</p>
<p>So now I have the date. I’m guessing the operation date will be 1-2 weeks after this. So around mid-July… or in other words around the time of the F1 at Silverstone… Damn you Gary!</p>
<h3 id="back-on-the-steroids---thursday-3rd-june">Back on the steroids - Thursday 3rd June</h3>
<p>Unfortunately over the past 2-3 days the symptoms in my legs and feet have returned. I was hoping that the anti-seizure drugs were the ones keeping these at bay, but it turns out it must have been the steroids which I came off a number of weeks ago. The team have decided to put me back on a short dose so as to improve my mobility (which I’m very grateful for!). I’ll be halving the dosage in a week, then likely coming off them again the week after. Since it takes a while for them to leave your system I’m hoping that takes me up very close to a surgery date, at which point the root cause of the issue can be ejected from one’s skull!</p>
<h3 id="reduced-seizures-and-steroid-side-effects---monday-7th-june">Reduced seizures and steroid side effects - Monday 7th June</h3>
<p>So the good news, the seizures seem to have reduced a lot over the past few days. I’ve had very little to write in my seizure log for the team. I like this direction of travel. Unfortunately as happened before the steroids have decimated my sleeping pattern. I’m currently writing this update at 00:40 and don’t feel tired at all. And yesterday I went to bed / sleep at around 3am, then woke up at 6:50am. Apparently under 4 hours of sleep a night isn’t the ideal amount you should be having! Oh well, at least I’m getting lots more stuff done!</p>
<h3 id="neurologist-appointment---thursday-10th-june">Neurologist appointment - Thursday 10th June</h3>
<p>Due to the seizures I’ve been having I was referred to the Neurologist who is part of the multidisciplinary team who are looking after me. It was a somewhat confusing meeting, but also very interesting that lasted around 40 minutes.</p>
<p>From previous conversations I’ve had with doctors, I was under the impression that the tumor was likely causing the issues I’d seen with walking in October (left back, leg, foot pain etc). But upon talking to the Neurologist and describing my symptoms, they seem to think that isn’t the case and it’s all purely coincidental. Confusing! I’d prefer it to be the tumor causing it to be honest. Having 1 major brain operation and solving 2 problems, sounds like a good deal! But alas, we shall see when Gary finally buggers off into the hospital incinerator. Only then shall we know for sure!</p>
<p>What was interesting for me was the conversation around classifying my seizures. I’ve been keeping a seizure log (yet another fun Google spreadsheet!). All fairly standard columns:</p>
<ul>
<li>Date</li>
<li>Time</li>
<li>What was I doing</li>
<li>(Copious) Notes</li>
</ul>
<p>Now I’d already started to notice this myself: not all my seizures are alike, and since taking the anti-seizure medication I’d started having different symptoms. The Neurologist confirmed this today. So I’ve now added another column:</p>
<ul>
<li>Is it a seizure involving the ‘core features’</li>
</ul>
<p>A convoluted column title I know, but I’ll explain what It means. The ‘core features’ are effectively the most obvious (and serious symptoms). For me I’ve identified them as:</p>
<ul>
<li>The feeling of being ‘outside my own head’</li>
<li>The emotionally driven feeling of everything being bigger than my own self (existentialism)</li>
<li>Any loss of motion and control over my body</li>
<li>Loss of consciousness (not happened as of yet thankfully!)</li>
</ul>
<p>Basically if I have a seizure that displays these core features, then it is logged as one. The other seizures I’m having are still logged in detail, but not listed as a ‘core feature’ seizure.</p>
<p>So why is this important? Well, without this classification every little seizure suddenly contributes to what looks like an escalating number of seizures. If this is the case then getting the balance for medication is difficult. I want to be on enough to stop the ‘core feature’ seizures, but not so much that I have other side effects. This classification allows me to pull out the really important ones and therefore allow the team looking after me to see if they indeed are escalating, or if they are being managed.</p>
<p>So there we go, top tip if you have a brain tumor. Keep a seizure log and look for patterns in what happens when they occur!</p>
<h3 id="a-core-feature-seizure---friday-11th-june">A ‘core feature’ seizure - Friday 11th June</h3>
<p>After the call yesterday it was all going so well. Nothing I would class as a ‘core feature’ seizure for the best part of 2 weeks. Unfortunately, one happened this evening at about 10 past 9pm. Watching the TV sitting on the floor, my wife just paused the TV to get something from the kitchen then I felt it. Again like an emotional response. Something was about to happen. I could feel it coming on. I tried to turn around and ‘shake myself’ out of it and convince myself I could stop it from happening. But within 5 seconds I’d lost control of my upper body and was shaking gently back and forth, unable to stop it.</p>
<p>As I’ve been told to see if my speech was affected I made sure my wife could hear me this time. She had walked in the room and not noticed what was happening so played the TV. I just started saying “Pause it”, “Pause it” “Pause it” over and over again, then eventually I said “I’m having a seizure”. Now I <em>could</em> have stopped myself from speaking, but I wanted to make sure we knew what the effect of it was on my speech, so I continued talking. It turns out she could hear me fine, no slurred speech. Said I sounded quite normal. I guess that’s a good thing? In total from the feeling to the end I’d guess it lasted about 30-40 seconds. Even 2 hours after it I still feel weak across the back of my shoulder blades and upper arms.</p>
<p>I’m hoping it was due to it being a long and busy week, maybe just tired. Or the fact that I’m now weaning myself off the steroids on the request of the Neurologist. I’m not sure! But to be honest, if I only get them once every 2 weeks, I’ll take that at the moment!</p>
<h3 id="a-sleepy-seizure---thursday-17th-june">A sleepy seizure - Thursday 17th June</h3>
<p>All has been quite quiet on the seizure front for the past few days. I’ve been gradually feeling weaker as the steroids pass through my system and my body takes over producing whatever it needs to produce… (can you tell I’m an expert in this doctor stuff!)</p>
<p>I’ve been getting very tired, very quickly. One moment I’m fine, 5 mins later I can hardly keep my eyes open. This happened around 5pm, just after having dinner with the kids. So I needed to go sit down in the living room. Within a few minutes I’d fallen asleep. While sleeping I’m 99% sure I had a seizure. Unfortunately due to the fact that I was asleep I’m struggling to classify it, as I’m unsure if I lost control of my arms etc, since I wasn’t moving.</p>
<p>The reason I believe I had one was because I remember coming too, and it felt like my brain split in two. Suddenly a pressure was released and my head felt ‘open’. I didn’t even realise I had that pressure there; it was only once it had been released I noticed the difference. I woke up a few minutes later and felt so much better, so maybe that’s how it works? The ‘pressure’ builds in my head and I don’t feel better until it is released. I’m not sure, I’m still getting used to the whole having seizures, and what triggers them.</p>
<h3 id="an-epiphany---friday-18th-june">An epiphany - Friday 18th June</h3>
<p>It’s hard to stop yourself thinking about everything that is going on. It’s one of the reasons I enjoy continuing to work. I get to escape from thinking about the current situation and feel ‘normal’ for a short period of time. Well, at least until I remember what is happening again…</p>
<p>So on Friday I had an epiphany about the whole situation. I came to the realisation that I’m dying. Now I know what you’re thinking… “Damn, that got dark very quickly!” I came to the conclusion via a little thought experiment.</p>
<p>Imagine the wonders of modern medicine didn’t exist. No drugs, no ability to operate and remove Gary. What would be happening to me in this situation? The reason I know about any of this at all is because the tumor has got to a sufficient size to cause the seizures, so it must have crossed some tipping point recently. The tumor would be getting bigger, the number of seizures I’d be having would be ever increasing. I believe the tumor would just be getting bigger and bigger, putting more and more pressure on my brain until it eventually caused so many issues that I passed away. I’m actually on a path in my life where this would be happening to me, but thankfully I have options to try to change this path.</p>
<p>I like to think of all this as water slowly filling a bucket. If the bucket overflows, bad things happen. At the moment the drugs have created a hole in the bucket to help stop it overflowing, but eventually the bucket will overflow, even with the hole. The only way to stop it from overflowing is to stop the water, and that basically means removing Gary. So thank goodness for modern medicine, else there’d be overflowing buckets all over the place!</p>
<p>Well that was an uplifting read wasn’t it? Thanks for reading, have a great rest of the day! :)</p>
<h3 id="a-very-good-roast-dinner---sunday-20th-june">A very good roast dinner - Sunday 20th June</h3>
<p>Due to covid I’d not been to see my sisters new house. Not so new anymore since she moved in in February 2020! With restrictions gradually lifting and testing readily available we’d decided to go visit and have a Sunday dinner. The kids loved the adventure, investigating the house and going to visit a new park just 5 mins away.</p>
<p>We ate around midday. It had been a while since having roast dinner, especially since it was beef, so I may have eaten a little too much too quickly. About 5 minutes after finishing while just chatting at the table I suddenly felt incredibly tired. I simply couldn’t keep my eyes open. My arms felt heavy and I found I couldn’t concentrate on the flow of the conversation. There was no immediate seizure, but it felt like something was happening. Thankfully my sister had a spare room with a bed so I decided to go lie down to try to sleep it off.</p>
<p>I woke up around 1.5 hours later feeling much better. As I’ve mentioned a couple of times before, I may have had a seizure in my sleep. The ‘pressure’ in my head seemed to lift it one point while sleeping, I remember it happening. It isn’t really ‘pressure’ as such, but it’s hard to describe. Something ‘lifted’ and I suddenly felt much better.</p>
<p>So there you have it. My sister now has quite a claim to fame. Her roast dinners are so good they gave her brother a seizure!</p>
<h3 id="the-baseline-memory-test---tuesday-29th-june">The baseline memory test - Tuesday 29th June</h3>
<p>At the beginning of the month I was told a date of the 29th of June for my memory test, so the whole month we’ve been waiting for this day. I wouldn’t say I’d been nervous. Really just anxious to get it ‘out the way’. I had no idea what was involved. All I knew was it was quite intensive and it would last a few hours. Thankfully you can’t revise for a memory test, as it did feel like being back at school waiting for an exam. The testing involved a chat with the neuropsychologist for 1 hour and then some intensive testing that lasted 2 hours.</p>
<p>The first part was easy. I enjoyed chatting about the situation to test myself and see what I could remember. And also to verbalise my understanding of the situation. Now I’m pretty sure the doctor had read all my medical notes, but there were points in the chat where I felt she was feigning ignorance about certain details. I’m guessing this is a tactic used to try to get me to explain my thoughts and feelings so they gather a greater understanding of my current mood and attitude to the whole situation. Not that I mind at all, it’s good to talk about all this. I often find I don’t know how I’m feeling until I try to explain it to another person.</p>
<p>The actual testing was really interesting. I was given around 12-14 different tests over a 2 hour period. Some were more difficult than others. There were tests that involved pronouncing words, and also remembering words. The tests always started very easy, then gradually got harder. So you’d start off thinking how smart you were, then by the end think the complete opposite! But I guess that’s the point when trying to look for a baseline result, each and every person doing the tests will find some more difficult than others.</p>
<p>I personally found one set of tests quite difficult. It involved thinking of as many words starting with a particular letter in 60 seconds. The only rules were they couldn’t be a person’s name or places. Any other word in the english language starting with that letter. There are that many surely it can’t be that hard! Well apparently not. For some reason my brain was insistent on thinking of swear words starting with whatever letter I was given. So each and every time I’d think to myself, “No I can’t say that word, it’s rude…”. Once you get into that little game with your brain (“say that, it would be funny!”), It’s hard to stop! Thanks brain! I think I could only think of between 10-15 words in 60 seconds. Not many at all so I’d say I’m below average for that test!</p>
<p>The easiest tests I found were the ones involving patterns. Look at this sequence of colours and shapes then pick the next one in the sequence from a list. I think I did okay in those tests, but who knows! Maybe I’m below average for those too.</p>
<p>The testing was intentionally hard as this is to try to gauge where you’re at and set that baseline. After surgery they can then repeat the test and look at the difference between the two sets of results. I guess since there were lots of different types of tests, if they see a drop in the results for one type of test, it suggests a change in a certain area of the brain. It’s all very clever!</p>
<p>Now with this date out the way, it’s just waiting on a date for surgery. All I know is that it is likely to be at some point in July. So July is going to be an interesting month!</p>
<h3 id="shielding---sunday-4th-july">Shielding - Sunday 4th July</h3>
<p>Unfortunately there’s been a number of positive covid cases at my children’s school. Year 2 and year 5 have been isolating at home for 10 days. We’ve been testing every day, and have been for a drive-through PCR test. Thankfully they all came back negative!</p>
<p>With both children back at school, and covid literally so close (there are a number of schools in Oxfordshire having similar issues), my wife and I decided that it would be best if I were to shield. It’s pretty crappy to have to avoid your own children, but having waited 2.5 months for a surgery date I really don’t want to lose it because I catch covid. That would be bad!</p>
<p>So I’m now staying with my parents. It’s the first time I’ve lived with them in 21 years! I’m still working and plan to do so, up until I get a surgery date. This will happen on a Tuesday at some point in July. Every Tuesday they triage the patient list and make plans for the week. So I’ll receive a phone call saying surgery will be on Friday! You don’t get much notice, only a couple of days. Although that’s probably a good thing. I don’t want to be dwelling on the fact that they are going to be removing part of my brain in a couple of days for too long!</p>
<p>When folks have asked me how I’m feeling about it all, It’s hard to describe. In some ways I want the surgery to happen right away to at least “get it out the way”. But in others I don’t want it to happen at all, as it really is a watershed moment in my life. There’s always going to be a ‘pre-surgery’ and ‘post-surgery’ me from beyond that point. Fingers crossed those two versions of me don’t change much!</p>
<p>I’m siding on the fact that I actually want it done now. My family and I have been in limbo for a few months now, so it would be good to be able to move forwards and start to actually plan things! I’ve gained so much respect for people who are on transplant lists for years. Waiting for hearts, kidneys, livers etc. I’ve only been waiting 2.5 months and it has felt like an eternity! I can’t even imagine living your life waiting years for surgery. Not being able to properly plan anything because “what if we get the call?”.</p>
<p>My next update will likely be when I have an actual surgery date. Not too long I hope!</p>
<h3 id="no-news---tuesday-6th-july">No news - Tuesday 6th July</h3>
<p>So having waited around all day for a call from the hospital, it unfortunately never came, so no surgery this week. Annoying but at least it gives me a little more time to get a few more things done! Let’s see what next Tuesday brings!</p>
<p>In other news, there’s been a few more reports of covid in schools in Oxfordshire so I still feel sheilding was the right thing to do!</p>
<h3 id="still-no-news---tuesday-13th-july">Still no news - Tuesday 13th July</h3>
<p>Mid-way through July now and still no news about surgery. I was hoping to have it at the start of the month so I could at least start the recovery process and maybe have the latter end of the summer to do something “normal”, (whatever that means in 2021!).</p>
<p>Being on the waiting list for hospital appointments is pretty stressful at the moment as <a href="https://www.theguardian.com/politics/2021/jul/12/boris-johnson-urges-covid-caution-amid-warnings-of-1000-hospitalisations-a-day">covid cases are surging</a>, I really feel for the folks working in the NHS at the moment. It must feel like a never-ending tsunami of stress and unpredictability.</p>
<p>Well at least I’ve been given a date of “July”, that’s a lot more than some people have! I’ve a sneaking feeling it will be right at the end. Fingers crossed the next few weeks go quickly. Although, saying that, these past few months have been the longest of my life!</p>
<h3 id="between-life-and-death---thursday-15th-july">Between Life and Death - Thursday 15th July</h3>
<p>Earlier in the week I discovered there’d been a short series on Channel 4 here in the UK called <a href="https://www.channel4.com/programmes/brain-surgeons-between-life-and-death">“Brain Surgeons: Between Life and Death”</a>. Reading the synopsis it all sounds very interesting. I thought it would give me some insight into what to expect on the day. Unfortunately I wasn’t mentally prepared to watch it. I made about 5 mins into the program and had to stop. It hadn’t even got to any of the surgery yet and was just talking about the people and their families. I think that brought it all home how serious this all is, not just for me but for everyone around me. What finished me off was seeing the guy lie down on the operating table and realising in only a few weeks time I’m going to be doing exactly the same. It’s not a pleasant position to be in.</p>
<p>I mean let’s be honest here, the clue is in the title: “Between Life and Death”. It probably isn’t the best thing to be watching weeks before major brain surgery! Saying that, I do plan on watching it post-surgery. I’ll have a very unique perspective of what it’s like from ‘inside’ the process. I can then compare notes from ‘outside’ the process too.</p>
<h3 id="close-but-no-brain-surgery---friday-23rd-july">Close but no brain surgery - Friday 23rd July</h3>
<p>So this week has been quite a rollercoaster. For the past 3 weeks I’ve been mentally preparing for a date of the 30th July for surgery. Now, I realise none of this is set in stone, but as that’s the only date you’re given it tends to be the one you focus on. Unfortunately I had an update at the start of the week stating that the date was going to have to be pushed back. It sounds like I was actually quite close, but a few emergency cases came in that took priority. So I’m back to the 2 week holding pattern (which really sucks!).</p>
<p>Saying that, I was given some interesting information during the conversations I’ve had with the team. Another MRI scan has been requested as I’m coming up to the 3 month point since diagnosis. But this isn’t your usual MRI scan. It’s actually being pitched as the pre-surgery scan. The neurosurgeon (and team) would like to understand what Gary has been upto over the past 3 months. From what I understand, this will be a more detailed scan that can be used for surgery planning. I don’t know if this means it will take longer, or if they just conduct more scans / more detailed analysis(?) I guess a question for the test operator when I meet them soon.</p>
<p>I was also given information that the operation is likely going to take longer than expected. The team usually conducts 2 operations on the Friday of each week. But the neurosurgeon is now considering only operating on me on the day, due to the size of Gary. I’ve yet to decide if this is a good or bad thing? Good in the fact that they can take their time to extract “the prick”, but bad in the fact that it is needed (and I’ll be awake for most of it!).</p>
<p>Fingers crossed the 6th August materialises. Folks have said to me that the waiting is the worst part. I’ve no way of knowing that at the moment, but I’m happy to agree. It is pretty bad! Let’s see what next week brings I guess!</p>
<h3 id="pre-surgery-mri-scan---monday-26th-july">Pre-surgery MRI scan - Monday 26th July</h3>
<p>Today I had my pre-surgery MRI scan. Now that suggests that I have a date for surgery, but alas no, not yet. I’m supposed to have an updated scan every 3 months to look at what Gary has been up to, so instead of having 2 scans in a short period of time, the standard scan has been upgraded to a pre-surgery scan. This means that the scan takes a little longer than usual (around 40 mins). They take more detailed scans and I assume maybe a few more types? This means there’s no ‘blocker’ for surgery now. When I get a slot, I’m not waiting on an MRI scan. This is good news!</p>
<p>The scan itself was fairly easy. There was a slight miscommunication so I was waiting around 30 mins longer than needed, but compared to how long I’ve been waiting for surgery it’s nothing. At least it is done! 40 minutes stuck in a cold, confined tube that screams at you like a ZX Spectrum isn’t the most relaxing experience. At 35 mins in they came in and injected the contrast into my arm via the cannula (I hate cannulas, so uncomfortable!). It was cold as it ran up my arm. A few seconds later I could swear I had a mini-seizure. I had the ‘emotional’ response I’m getting used to, but since my head was tightly strapped to the MRI table and I was lying down in a confined tube, I’m not sure if it affected my movement. I’m going to raise this with the next doctor I speak to. Maybe it’s a known side effect?</p>
<p>As another Tuesday rolls around, here’s hoping I get any news on a date for surgery. The wait is really starting to get me down.</p>
<h3 id="one-confusing-week---sunday-1st-august">One confusing week! - Sunday 1st August</h3>
<p>The last week has all been a little crazy. On Monday I had my 3 month MRI scan, which was also the pre-surgery MRI scan too. I’ve mostly been on annual leave from work as there’s just so much going on it’s hard to concentrate on anything else.</p>
<p>On Tuesday I was back to work for the day, but something unusual happened. I was in a meeting with my incredibly supportive line manager, but 30 mins into our 45 minute video call I had to stop quite abruptly. I completely lost my train of thought and felt like I was on the cusp of having a seizure. Thankfully I can now recognise the telltale signs of this happening. This is a worrying sign as work has been a haven for me over the past 3 months. I’ve been able to zone out of “real life” for a few hours and concentrate on work, completely oblivious to the fact that Gary even exists. For a few hours at least! It now seems that even this little haven is eroding.</p>
<p>On Wednesday I had a call from the hospital with the results of my Monday scan. It’s a bit of a mixed bag. The good news is that the tumor hasn’t grown in the past 3 months. The less good news is that there’s still no date for surgery. The bad news is that some swelling can be seen around the tumor, also known as cerebral edema. I think this explains the symptoms I saw on Tuesday.</p>
<p>Thursday and Friday were a bit of a writeoff to be honest. All day I wasn’t feeling 100%. Incredibly sleepy, dizzy and at times quite confused. It’s strange, and it comes on so quickly. One minute you feel fine, next minute it all changes for no apparent reason. Thursday evening was particularly bad, where I could barely keep my eyes open or even really speak to my wife. I was completely disconnected from reality and didn’t have the energy to even try to re-engage.</p>
<p>On Friday evening it got so bad that my wife decided to ask for advice from the 111 team (non-medical emergency number). We went through the triage process and eventually the very nice on-call GP was sent out to see me. She was satisfied I didn’t need to go to hospital but did want to phone the specialists in the oncology and neurology departments. On speaking to the on-call neurology consultant who looked at my scans they recommended that I should be back on steroids to reduce the swelling in my brain.</p>
<p>Now here’s the difficult part. The team I’ve been under the care of for the past 3 months have been very keen to take me off steroids when I’ve been on them, since they are quite strong and have some nasty side effects. Steroids take around 2 weeks to leave your system once you take them. So I’ve been stuck between a rock and a hard place. Take the steroids and I could potentially compromise a future surgery date. Don’t take them and my symptoms could get worse.</p>
<p>Weighing up these options, it was 100% my decision NOT to take the steroids until I’ve been given the all-clear from the team looking after me at the hospital. There’s no way I want to lose the opportunity of surgery because I took a drug that is now in my system for weeks. So due to the fact that I was having unpredictable symptoms from the swelling, I’ve moved back with my parents for a few days. At least they can easily keep an eye on me without having to keep two young children entertained.</p>
<p>So there you have it, quite an eventful week! I’m seriously hoping that the upcoming week is the one where I’m given a surgery date. I don’t think I can cope with much more waiting around “treading water”. Roll on Tuesday. Fingers crossed <em>the</em> phone call!</p>
<h2 id="the-brain-fog-has-lifted---friday-6th-august">The brain fog has lifted! - Friday 6th August</h2>
<p>Yet another crazy week with quite a few highs and lows. I’ve been using some of my annual leave to get a few things sorted before the surgery. If I’m honest, I haven’t managed to do much for the past few weeks which is very frustrating. Being so tired and dizzy makes it hard to concentrate. I’ve been sleeping a lot and very rarely left the house.</p>
<p>Tuesday came and went with no real updates about a surgery date which was very disappointing. I did hear back from the team at the hospital about the steroids, so from Wednesday morning I started on them again (to reduce the swelling around the tumour). Wednesday and Thursday there was some improvement like they were beginning to make a difference. I had much more energy and I could finally walk properly again (as the issue with my foot has returned).</p>
<p>But it was today (Friday) where I’ve really felt better. The best I have felt in weeks to be honest. I realised while having a shower this morning that I was actually thinking and planning out my whole day ahead of time. Multiple streams of thoughts about everything I wanted to get done. I’d not had this ability for a few weeks. It has literally been quite ‘single threaded’ (there’s a web developer term for you). Basically I’ve only been able to focus on the single task at hand, nothing more. I must admit I didn’t notice this change, it was only once I ‘reverted back’ to feeling normal again I realised it had happened. So yes, I went into overdrive with work today because I could finally do things again! I worked until 7:30pm (naughty I know) getting tonnes of stuff sorted, and started making plans for the department’s stance on legacy browsers (cough… IE11), which should come into effect in March to June in 2022. So planning almost 10 months ahead of time. I simply couldn’t have done that even 3 days ago.</p>
<p>Next week is going to be interesting. I have a couple of meetings with the hospital to go over symptoms and then speak to the neurosurgeon again. I have lots more questions for the team. So let’s see what next week brings! Maybe some clarity on a date, like I’ve had from the steroids over the past few days. That would be nice!</p>
<h3 id="another-mixed-few-days---sunday-8th-august">Another mixed few days - Sunday 8th August</h3>
<p>Well it has been an odd weekend symptoms wise. Friday was a very good day. The best one I’d had in a while actually. I could finally think clearly again. Unfortunately I think I went a little bit overboard!</p>
<p><strong>Saturday</strong></p>
<p>I really paid the price on Saturday. As Friday had gone so well, we decided to head out to an open day event for a charity. We were under the impression it wouldn’t be particularly busy, but unfortunately not. It seemed like everyone had turned up. This isn’t a bad thing in terms of covid as it was all outdoors and we all kept our distance. It was just the sheer number of people, sounds, noise, interactions… the general busyness of it all that broke my brain.</p>
<p>I lasted about an hour then had to admit defeat and go back to the car to sleep with some quiet music on for 2 hours. I believe this was a mixture of the tumour, swelling, and having just started taking steroids. Apparently steroids can heighten your senses, making sounds louder, colours brighter etc. I think it was a combination of it all that my brain just needed to get out of the situation.</p>
<p>Thankfully after a couple of hours of rest and getting home I felt much better, and ended up doing virtually very little for the rest of the day.</p>
<p><strong>Sunday</strong></p>
<p>After a great Friday and poor Saturday we decided that I best not risk doing much, so I mainly pottered around the house looking for little bits to keep me occupied. I’ve certainly noticed that I’m much more comfortable and settled while at home. I’d assumed it was all a part of the past 18 months and the pandemic, but I’m beginning to think it is all interrelated. I think (unknowingly) to me this tumour has been developing and growing and it has been changing how my brain works and thinks. Although I don’t feel anxious about leaving home, there’s always something in the back of my head where I can only fully relax once I’m back.</p>
<p>Now, day to day you don’t notice this, why would you after all? Such small changes. It’s just like watching your children (or pets) grow up. Day to day you see no change, but looking back everything has changed. So when these subtle differences are compounded over months (and even years), then you see the huge change staring you in the face. The more I think about it, the more it makes sense. My brain is actively looking for a place with the least cognitive load. Familiar surroundings and people with complete predictability. This equates to a brain that doesn’t need to do much (which is good when you have a brain tumour!).</p>
<p>Annoyingly on the flip side, the steroids seem to give me enough energy to put me on edge (almost twitchy), but then I don’t have the energy or brain capacity to do anything particularly useful with it. Stuck between a rock and a hard place really. So the day was mainly resting and mentally preparing myself for another week of uncertainty.</p>
<h3 id="the-week-that-will-never-end---wednesday-11th-august">The week that will never end! - Wednesday 11th August</h3>
<p>Well, the title says it all really. This feels like it has been a very long week. I’ve had 2 important meetings at the start of the week, and after those it’s hard to believe there are still more days left to go!</p>
<p><strong>Monday</strong></p>
<p>Since starting back on the steroids last week to reduce the swelling on my brain the team has wanted to keep a close eye on me. If there’s one takeaway from the past 3 months it’s that they don’t want me on them! That’s fine with me, I’m happy to listen to the experts!</p>
<p>So I had a general catchup with the nursing team going over my symptoms since I started back on them again. I explained the highs and lows of the weekend and it all sounds like classic steroid side effects. One day you feel on the top of the world, the next day you are back in the depths. When you couple it together with the tumour, swelling, and the fact that steroids obliterate my sleeping pattern (3-4 hours a night when on them), it all makes sense really. I’m on mild sleeping tablets to combat the issue, but they don’t work too well.</p>
<p>The rest of the call was mainly to prepare me for the Tuesday call with the neurosurgeon. I’d already started making a list of questions, but off the back of the conversation I now had a few more. The more information you can get from the person who will actually be operating on you the better really!</p>
<p><strong>Tuesday</strong></p>
<p>I worked in the morning as the call wasn’t until the afternoon. For some reason I was very nervous about the whole thing, so I was glad to have the distraction. I’m not quite sure why I was so nervous, but it’s always in the back of my head: “maybe they made a mistake, and the 2nd scan will show a much worse prognosis”. Unlikely I know, as I’m sure they would have mentioned it, but it’s hard to stop thinking about it!</p>
<p>But overall the 45 minute conversation went really well! There’s been no growth in the tumour size over the past 3 months which is amazing news. The tumour still looks to be of the type oligodendroglioma. The fact it hasn’t grown point towards it still being grade II and not anaplastic oligodendriogliomas (grade III). We still won’t know this for sure until 10 days after surgery, as that’s how long it takes to get biopsy results back.</p>
<p>The swelling around the tumour isn’t as bad as thought. Only slight swelling really. But this then points to the fact that the symptoms I’ve been getting (dizziness, ‘fuzzy head’, loss of concentration, disconnection with the world etc) all relate to the tumour itself. I’m not sure which I prefer it to be, the tumour or the swelling!</p>
<p>I got to ask lots of great questions and get most of the answers I needed. One of the fundamental takeaways is that I “must not get covid”, (direct quote from the neurosurgeon). I’m more than happy to keep to that! Having been double jabbed and not really going out, I’d hope the chance is pretty slim anyway.</p>
<p>The main discussion was around a date for surgery, and this came up almost right away. The team were very apologetic that it had taken so long, but the pressures on the whole system at the moment are large. Now as I seem to have a grade II tumour that is large (but not aggressive) this drops me down the priority list somewhat. There are some types of brain tumour like the <a href="https://en.wikipedia.org/wiki/Glioblastoma">glioblastoma</a> that are really really aggressive and can’t be cured. The neurosurgeon explained that in these cases sometimes you only have a matter of days / weeks where you can make <em>any</em> difference to a patient’s life expectancy! So at the moment I’m now top of the list, barring any emergency cases coming in. So in theory I should <em>hopefully</em> be operated on in the next two weeks(!). Off the back of the meeting we are looking at a date of “August”. If this is the case, it fits well with the children going back to school in September, as I can see the risk of getting covid in this period increasing once they are back. And what with my instructions “not to get covid”, these two worlds don’t fit together!</p>
<p>I asked about how big he thought my tumour was. I’ve already exported a 3D animation of Gary which you can <a href="https://twitter.com/TheRealNooshu/status/1397459707422547968">see here</a>, but I wasn’t sure actually how big ‘he’ is in terms of numbers. The neurosurgeon estimates that ‘he’ is taking up around 10-15% of my brain. That’s pretty impressive! So I only have the capacity to use 85-90% of my brain anyway. I also asked the question if this is the type of tumour I could have had since birth, and I’ve just grown up with it? The answer to that was a blunt “no”, so that’s the end of that conversation then!</p>
<p>I found out a more about the operation itself and recovery period:</p>
<p><strong>Operation</strong></p>
<p>It is likely to take 6-9 hours, with me being awake for approximately 6 of those hours. That sounds like a hell of a long time to me, but the neurosurgeon assures me it will go quickly. He said I’d be surprised how quickly it passes, so watch this space. I’ll let you know after the event! A successful operation is where he manages to remove around 80% of the tumour, leaving the rest to be removed via other means (radiotherapy / chemotherapy / additional surgery).</p>
<p><strong>Recovery</strong></p>
<p>Now because the tumour is so large and deep into my brain, it comes with a few risks (as I’ve mentioned before in the original post). One of the main risks is the fact that the tumour is wrapped around my motor cortex, which is the part of the brain that controls limb movement. As Gary is sitting in my right frontal lobe, any issues will occur on my left hand side (since the brain is reversed in this respect).</p>
<p>The neurosurgeon explained that there is something like a ‘switch’ that can effectively turn on and off if the brain is damaged (or changed). During the operation there’s no warning that this will occur. One moment I could have full control over my left arm / hand, the next moment nothing. Quite a scary thought really! Thankfully in most cases this is only temporary and lasts 10-14 days. It’s very rare that it is permanent. Over time, as the brain re-adjusts to being without the tumour, it relearns how to process limb movement and the ‘switch’ is turned back on. If this were to happen, my seemingly ridiculously short 24-48 hour stay in hospital will turn into a couple of weeks in rehabilitation. So there’s just another unknown to throw into the mix!</p>
<p>So that was mainly what I learned from the first two days of the week! Quite a lot to take in, and there are still 3 days left to go! I plan to take it a little easy to process it all and start mentally preparing myself for the next two weeks, where there’s a lot that can happen very quickly indeed. I feel I’m about to reach a watershed moment in my life. There’s forever going to be a pre-surgery me, and post-surgery me on the horizon.</p>
<p>Tune back in soon for more on the eviction of Gary ‘the prick’ Glia!</p>
<h3 id="houston-we-have-a-date---tuesday-17th-august">Houston, we have a date! - Tuesday 17th August</h3>
<p>So it’s been over three months since I was first diagnosed, and I must admit I’ve no idea where the time has gone! In some ways it feels like it has gone quickly, but in others it feels like it has been the longest three months of my life! Living your life in weekly chunks isn’t much fun. Waiting for every Tuesday to come around for a surgery date is quite stressful. The day comes and goes with a whole range of emotions. A mixture of anxiety, excitement, all the way through to disappointment and sadness, then rinse and repeat for another week.</p>
<p>Thankfully the waiting now seems to be coming to an end as I’ve finally been given a date for the operation: <strong>Friday 27th August</strong>, or G-Day as I’m calling it. G-Day stands for “Gary get the f*ck out of my head”-Day. The plan is for me to go into hospital the day before and stay overnight, with the operation happening on the Friday morning. It should last approximately 8-9 hours. Beyond that is a big unknown, so we’ll have to see how it goes!</p>
<p>Unfortunately, there is always a chance that it could be cancelled at the last minute, or the hospital doesn’t have a bed for me. If this happens there are multiple options. The nurse explained: “Plan A is the aim, but plans B-E also exist too”. We will only know the plan I’m sticking to on Thursday. It will be a relief to get a hospital bed, as that’s the final step before surgery!</p>
<p>In other news: I also found out that during recovery I’m going to need to wear deep vein thrombosis (DVT) stockings for 1 month(!) after surgery. Night and day, 24/7! Apparently the risk of blood clots post-brain tumor surgery is high, so to reduce the risk the stockings are required. Now as a person who doesn’t even like to wear socks to bed, this sounds horrendous! But if it means I don’t get blood clots, well I guess it’s a thing I’m going to need to get used to. I think the next few months are going to be very difficult. A character building experience some might say…</p>
<h3 id="last-day-at-work---friday-20th-august">Last day at work - Friday 20th August</h3>
<p>Today has been a strange day. It was my final day “in the office”, as I’m stopping work to prepare for my operation next week. I have to admit I’m feeling a little sad about it. Work has been a real safetynet over the past few months. When I needed a distraction from all this, and an “injection” of normality I could always fall back on work. The work isn’t easy. In fact it is incredibly frustrating at times. But that’s part of the distraction. Being able to make plans for the department in the coming years is time consuming and slow, but ultimately very rewarding. I’ve handed over as much as I can and tried to remove myself as “a blocker” from as many areas as I can, but it could be that certain areas stall until I get back to finally drive them forwards. Here’s hoping for a quick recovery and staggered return to “normality”, whatever the hell that is in 2020/2021!</p>
<h3 id="houston-we-have-a-seizure---friday-20th-august">Houston, we have a seizure! - Friday 20th August</h3>
<p>Just when Friday wasn’t strange and emotional enough, Gary steps in and makes things worse. 6:30pm rolls around and I’m making a cup of tea in the kitchen. I suddenly feel something “isn’t right”. I’m standing and realise I need to sit down quickly before it happens. I start to walk the few meters across the kitchen to the nearest chair but don’t make it in time. My body goes into a focal (partial) seizure.</p>
<p>As I’m walking I say to my wife: “I need to sit down”, but the words came out slurred (they sounded fine to me). Luckily I manage to lean on the kitchen worktop when it happens, so as not to fall over. I gently start to rock back and forwards, this is out of my control. At that point Claire told me to keep speaking, as it’s good to know what effect the seizure is having on my speech. At that moment I actually struggled to speak. That isn’t through lack of ability, it was through lack of “being bothered”, would be the best way to describe it. There seemed like a real disconnect between myself and reality which I didn’t have the energy or inclination to break down at the time.</p>
<p>The seizure seemed to pass quite quickly. Probably 20-30 seconds in total. I managed to sit down and take in what had just happened. Annoyingly I didn’t feel any ‘release’ of pressure in my head which I’ve had in the past sometimes. I actually didn’t feel any difference at all.</p>
<p>So this was the first clear ‘core function’ seizure I’ve had since the 11th June. There are two intriguing similarities between them: they both happened on a Friday after work, and both happened while coming off steroids. Now this may be a complete coincidence? I have no idea. Maybe I’ll ask the team next week. Next Friday can’t come soon enough!</p>
<h3 id="time-slows-down---sunday-22nd-august">Time slows down - Sunday 22nd August</h3>
<p>I knew this would happen. As soon as I had a date everything would shift. Suddenly time slows down as I’m no longer living each day waiting for a phone call, I’m living each day with a future date on ‘x number of days’ away. It certainly doesn’t help that sleeping is difficult. Only sleeping 3-4 hours a night makes for very long days! I think this may be the longest week of my life. I need to find a distraction (that isn’t work) to keep me occupied. Roll on next weekend and the Belgian Grand Prix. I will never be so happy as to be able to watch a Grand Prix knowing surgery is over!</p>
<h3 id="cancelled-surgery-1---tuesday-24th-august">Cancelled surgery #1 - Tuesday 24th August</h3>
<p>So today was a bit of a roller coaster. I woke up very early (I’m still not sleeping too well), as I needed to complete my PCR test at 7am. This works out as 3 days before surgery on Friday. Once completed I went for a little walk to post it in a priority post box. I saw absolutely nobody at that time of the morning, which was handy for COVID. I just so happened to walk past my old secondary school too, which was a trip down <a href="https://www.merriam-webster.com/dictionary/memory%20lane">memory lane</a>. Not much looked to have changed apart from some of the dilapidated classrooms have been knocked down and replaced with sports courts. I remember them being bad in 1998, I hate to think what they would have looked like 23 years later!</p>
<p>Unfortunately the positive start to the day wasn’t to last. A few hours later I received an email from the hospital stating that my operation on Friday had been cancelled due to a staffing issue with the team. I do wonder if this is all related to COVID, as it seems very strange that an operation planned 1.5 weeks in advance suddenly doesn’t have an important member of the team available. This is disappointing, but in all honesty I’d much prefer they are 100% happy with the team and everyone is available, rather than a ‘settling’ for a team just to get the operation ‘done’. So for the sake of another few weeks (after waiting 4 months), that’s fine by me!</p>
<p>Note that I’ve called this update ‘Cancelled surgery #1’. I’m hoping that there won’t be a #2!</p>
<h3 id="the-end-of-a-long-week---friday-27th-august">The end of a long week - Friday 27th August</h3>
<p>Today was supposed to be the day of my surgery. Unfortunately it was cancelled on Tuesday. In some ways I’m glad it was cancelled so early into the week, as that has given me time to get used to it all. If it had been cancelled the day before, that would have been even worse.</p>
<p>I’ve now been signed off work by my GP until my surgery date. I’ve managed to work for over 3 months, but the combination of the drugs and tumour are starting to take its toll. I can have dizzy spells and get tired very easily. One minute I feel fine, 5 minutes later I’ll feel totally different, almost drugged / drunk (I don’t drink). This all has had an impact on my concentration and work. This is especially true now that I’m off the steroids. They gave a little boost that enabled me to power through, but without them it’s difficult.</p>
<p>So all in all, a very long week. I’ve started making plans for the coming weeks as I’ve been given a very loose date of the 10th September for surgery. It’s not guaranteed, but it’s the only date we have to work towards at the moment, so it will have to do. With the children going back to school on the 2nd September I can see this being an interesting few weeks ahead!</p>
<h2 id="a-promising-end-to-the-week---saturday-4th-september">A promising end to the week - Saturday 4th September</h2>
<p>It has been a very busy week, lots going on! In preparation for recovery we’ve had a third of the garage converted into an office. This frees up a room that can be used as a spare bedroom. This will be really useful if I need friends and family to stay and help during this period. Because of this, I’ve been gradually cleaning up and making sure we can find things. I can’t explain how frustrating it is not to be able to find tools in the garage. A 5 minute job takes 20-30 minutes just because I can’t find the tools! Saying this, it’s all looking great, not 100% yet, but I’ll be able to potter about during recovery and finish it all off.</p>
<p>My kids are now back at school so I’m self-isolating at my parents. The risk of me getting COVID has jumped since we can no longer control or monitor who the kids are interacting with. Since it takes around 48 hours for COVID to become detectable using a lateral flow test, I could easily catch it and not know about it for a few days. This would be bad in terms of the operation. It puts me at risk, as well as the whole 12-14 people team who will be involved in the operation.</p>
<p>I had some really positive news on Friday when the neurosurgeon who will be forcefully ejecting Gary, phoned me. He phoned for 3 reasons:</p>
<p>To apologise for the cancellation on the 27th.
To check on how my seizures are at the moment.
To see if I would be interested in taking part in a new feasibility study related to more detailed brain imaging.</p>
<p>In relation to point 1, although this was disappointing, I really appreciate the fact that he made the difficult decision to cancel surgery. It was made because a critical team member wasn’t available. I’d much prefer to go into a long operation with the correct team, than a team pulled together just to ‘get the job done’. This makes me feel more comfortable about the surgery, both from the fact that I know the best team is involved, and also that the neurosurgeon has the team he wants around him. This must be a confidence boost for him too. Not that I expected (or even needed) an apology, it’s nice to be given it. The NHS are doing a fantastic job considering the tidal wave of craziness they have been under for the past 18 months!</p>
<p>Point 2 is an interesting one. It’s been a few weeks since I’ve had a ‘core feature’ seizure, but that doesn’t mean my brain hasn’t tried! There are certain days where I feel quite ‘detached’ from the world. If not ‘detached’, then I can be quite fuzzy headed where concentration is hard. I’m no expert, but this seems to me as though my brain is trying to have a seizure, but the drugs I’m on are stopping it. I keep a detailed seizure log and have been keeping the hospital team constantly updated with how I’m feeling day to day.</p>
<p>There have been discussions this week about upping my drugs, or even putting me on a second drug to try to combat these episodes during the operation. If I were to have one during the operation it could be bad! Although now it sounds like the decision has been made not to change my medication. I’m personally not concerned either way. I’m happy to follow the team’s advice. They are the experts afterall! According to the neurosurgeon it could be that the operation is adjusted to account for these ‘almost’ seizures, this could mean I’ll be awake for less time, which is safer if I do have a seizure. But it also means they won’t be able to take out as much of the tumour because they won’t know if they are inadvertently taking out ‘good’ parts of my brain. Ultimately it’s the neurosurgeon’s decision, and he will be making it on the day. I just need to keep the team informed as to how this week goes. That’s all I can do!</p>
<p>Point 3 is super exciting. I’ve been offered the chance to take part in a new feasibility study related to Fluorodopa <a href="https://en.wikipedia.org/wiki/Positron_emission_tomography">PET Imaging</a> in Glioma. I’ll be administered intravenously with a mildly radioactive drug, called <a href="https://en.wikipedia.org/wiki/Fluorodopa">fluorodopa</a>. The PET scanner then detects the radiation within the tumour. This imaging should give the team a lot more information about the internals of the tumour that a MRI & CT scan can’t show. So for example, the more aggressive parts of the tumour will show up on the scan, allowing the neurosurgeon to focus on those areas and make sure they are removed as a priority. In this instance the more data the team has the better, so I’ll be consenting to taking part in the study. It’s great to think that these scans could help with diagnosis of tumours in other people in the future. I’m also keeping my fingers crossed that I will also have access to these more detailed scans when I request them all post-surgery.</p>
<p>All in all a very busy week! Next week could be G-day week! We shall see, I expect to hear more on Tuesday, so watch this space. Gary, your days could be well and truly numbered. 6 days and counting (hopefully!).</p>
<h2 id="mri--pet-scan-day---wednesday-8th-september">MRI & PET scan day - Wednesday 8th September</h2>
<p>So it’s G-day + 2, and all is still looking good for Friday! I had to sign a lot of forms today related to the feasibility study and the actual operation. It was mainly going over the risks associated with surgery and what to expect. I’ve now been put on a 2nd type of seizure medication to try and head off the last of the seizure symptoms I’m having (pun intended!). My first PCR test came back clear, and I had another PCR test today. I will most likely have another 2 this week. My poor nose!</p>
<p>The MRI scan went well today. It lasted around 40 minutes. Thankfully I didn’t have a reaction when they injected the ‘contrast’ this time which is great. It was loud but tolerable. The doctors and staff at the hospitals (I went to 2 today) were all fantastic. I couldn’t wish for a better team. I feel very fortunate to live in Oxfordshire.</p>
<p>The PET test unfortunately wasn’t as pleasant. It was nothing to do with the fluorodopa, it was the fact that CT scanners really aren’t built for comfort. The scan lasted a whole hour, and honestly it felt like a lot longer as I had no reference point for the time! I was strapped to a machine with my head restricted and my feet were cold. The worst part was that there didn’t feel like there was enough padding behind my head. After 10 minutes the back of my head started to hurt, and once you notice it, it’s hard to stop focusing on it. Especially when there’s nothing else to do! Try it some time, it takes a surprising amount of energy to lie perfectly still for 1 hour while staring at the ceiling. I’m very glad it is now done though. Apparently the scans came out really well and they got all the data they needed for Friday’s operation!</p>
<p>I also found out a little about the surgery itself. I’ll be lying on my left side for the operation with some sharp points tightly pushed against my skin to stop my head moving. I’ll have a cannula in my right arm, as they want me to be able to move my left arm when I’m asked too. Since there’s a risk of partial paralysis on my left side, they will be testing for this throughout the operation. It sounds like this could be quite uncomfortable on my neck which is a worry, but I’m hoping I’m drugged up enough to really know about it!</p>
<p>So anyway, this is likely to be my last update for a little while, as I should be heading into the hospital around midday tomorrow. Fingers crossed Friday all goes well and I’ll be able to post updates soon. This should include the PET scan imaging as well as a before / after image of my brain, minus Gary!</p>
<p>No matter what happens, thanks everyone for all your support, kind messages & visits over the past 4 months. They have been very much appreciated. Here’s hoping the rest of 2021 / 2022 improve!</p>
<h2 id="thrust-into-the-post-surgery-world">Thrust into the post-surgery world</h2>
<p>So apologies to readers waiting for an update, it’s been quite a few months as I’ve been a little busy in recovery, a lot of things have happened so this is going to be a long update! I’ll pick up from where I left off on what I can only describe as the longest week of my life!</p>
<h2 id="into-hospital-maybe---thursday-9th-september">Into hospital maybe? - Thursday 9th September</h2>
<p>So Thursday was a <em>very</em> long day. I hadn’t been sleeping well all week, probably due to a combination of drugs and the stress of waiting for the operation. The fact that I was still having seizure-like symptoms didn’t help either. So I found myself awake at 3am thinking what can I do with my time as I didn’t think I’d be able to sleep. So I decided to do something I’d been putting off for months, simply because it isn’t pleasant and not what you expect to be doing at the age of 39 years old. So I planned my own funeral.</p>
<p>My sister is the executor of my will, so I wrote a fairly brief email for her, listing my basic wishes. For example:</p>
<ul>
<li>Cremation or burial</li>
<li>Religious or non-religious</li>
<li>Music / Dress Code</li>
<li>Flowers or donations to a (brain cancer) charity</li>
<li>Budget plans for payment</li>
<li>Where to spread my ashes</li>
</ul>
<p>I titled the subject line of the email “Unpleasant email” and used Gmail’s scheduled send functionality to send it at a more sociable time. In hindsight I should have set the subject to ““Unpleasant email [Don’t read in public]”, as I think I caught her a little off guard. It turns out she read it while in the supermarket, which unfortunately made her cry down the bread aisle of her local Sainsbury’s. I found this out in her reply back to me later in the day. The email chain is still difficult and upsetting for me to read even many months later.</p>
<p>So there you have it, learn a lesson from me: When you are sending an email about your funeral to your relatives make sure you consider when and where they might be when they read it! You can use the short checklist above as a starting point for your planning. Depressingly (for a 39 year old) I got much of this information from a checklist on the <a href="https://funeralplan.ageuk.org.uk/viewpoint/funeral-planning-checklist">AgeUK website</a>.</p>
<p>After that I still couldn’t sleep so I wrote a draft “Final” blog post for this very blog, as I’d already arranged for it to be published by a good friend and colleague at work should the worst happen. Thankfully as you can probably tell this preparation hasn’t been needed! On a more positive note I now have an idea of what I want my funeral to be like at the ripe old age of 39!</p>
<p>Let’s move past this deeply depressing section because the Thursday hasn’t even started yet! Due to the COVID risk from my children I’d been staying with my parents for a few weeks. So my parents and I would be making the 1.5 hour journey back to Oxfordshire in preparation for going into hospital later in the day (2pm).</p>
<p>Earlier in the month I’d had multiple phone calls with the team looking after me as to how the days before surgery would go. We had a “Plan A” all the way to a “Plan E”. Plan A was: I’d phone the hospital at 2pm the day before and a bed would be available. Travel to the hospital that day and stay overnight before the operation first thing on Friday morning. Of course nothing ever goes to plan! I phone at 2pm and there’s no bed available and nobody to really speak to about it.</p>
<p>At this point I was in the middle of feeling terrible due to my brain wanting to have a seizure but the drugs stopping it from happening. So I went to bed for a few hours to try and sleep it off. During those hours my wife and eventually my mother tried to get a straight answer from the hospital to see if I would have a bed that night. Around 7pm we got confirmation from the hospital to say that a bed wasn’t available but the operation was still going ahead in the morning. So I’d be sleeping in my own bed the night before the operation and leaving early for the hospital in the morning. In hindsight this was actually a good thing as you will read later in the update.</p>
<p>I’d just like to point out that this isn’t a dig at the excellent work all the NHS admin staff are doing. If there are only a limited number of beds available there’s not much you can do! Especially during Covid, which was on the rise in Oxfordshire at the time.</p>
<h2 id="day-of-days-g-day-is-finally-here---friday-10th-september-day">Day of Days (G-day is finally here) - Friday 10th September (day)</h2>
<p>G-day stands for G(ary get the f*ck out of my head)-day. And it’s all a bit of a blur to be honest. I woke up early as again I couldn’t sleep due to nerves. I was hoping to sleep a little in the car but wasn’t feeling well at all. My brain felt ‘fuzzy’, I couldn’t concentrate and had very little energy. I couldn’t have any breakfast due to the operation being in the morning so that didn’t help either. We set off early so as to avoid any traffic. The last thing I needed was to get stressed because we were late!</p>
<p>When we arrived at my home I finally got to see my wife and children for the first time in a few weeks. Covid and cancer really don’t mix well! My wife and I had already decided that she wouldn’t be coming to the hospital with me. She knew she would get upset, which would likely upset me too, which isn’t what you need just before a major operation. So she stayed at home and took the kids to school, to give the impression it was just another uneventful day! They knew that “Daddy was going into hospital to have the lump in his head removed”, but being young didn’t fully realise the seriousness of it all.</p>
<p>I needed to be at the hospital for 9am to be admitted to the ward for the operation prep. Once there nobody was at the reception to admit me so I ended up speaking to another patient waiting there. He was also being operated on today by a different team. He’d already had brain surgery 14 years earlier, the scar was clearly visible on his head where his hair hadn’t grown back. We made small-talk for a while but as we were both nervous it felt quite forced.</p>
<p>The reception staff arrived and admitted me. I had a brief consultation with the consultant anesthetist as he wanted to check I hadn’t eaten anything and wasn’t allergic to any medicines that I knew of. All standard questions for pre-surgery. Interestingly this is the consultant anesthetist who wasn’t available for my previous surgery date due to him being in Covid isolation. I guess the Neurosurgeon has a team that he likes to have around him, especially considering how big my tumour was and how long the operation would be! After the consultation I waited in reception for 5 minutes or so until one of the team led my mom and I to the bed I’d eventually be in after surgery.</p>
<p>The other patient who I’d been speaking to was in the bed opposite. Knowing that I wasn’t on my own my mom left and went back to the car where my dad was waiting to take them home. It all happened so quickly we both said our ‘final’ goodbyes and that was that. I was more worried about her getting lost in the hospital than the actual operation at that point.</p>
<p>Now I’d only been on my own for 1 or 2 minutes and the nurse came in to start pre-surgery prep. She took my blood pressure and oxygen levels etc. Then I had to put on what would quickly become the bane of my life for a month: <a href="https://www.healthline.com/health/dvt/compression-stockings">Deep Vein Thrombosis (DVT) leggings</a>. The reason for wearing these is that stats show that after brain surgery you are 30% more likely to develop blood clots which can be fatal if they travel to the heart or brain.</p>
<p>Once prepped I got to meet my Neurosurgeon for the first time in person, he introduced himself and checked I was doing okay. Usually I would have shook his hand but due to Covid I didn’t think it was appropriate. You’d be surprised how much a little thing like that annoyed me. You’re about to put your whole life and future into this person’s hands and you can’t even thank them with a handshake.</p>
<p>The Neurosurgeon left, and before I knew it I had a group of people around me leading me down to the operating theatre. I said goodbye and ‘good luck’ to the other patient in the room and out of the ward we went. I have to be honest I don’t remember the walk to the operating theatre at all, or what we discussed on the way there. My brain was just trying to take in the enormity of the situation ahead of me. After 6 months of mentally preparing for this moment it is finally here!</p>
<p>We entered the side room to the operating theatre and the team started to work on me. They told me to get up and sit on the trolly in the centre of the room. I distinctly remember thinking “This could be the last thing I ever do as a person”, It took all my courage to get onto the trolley and I have to say it’s the scariest thing I’ve ever had to do in my whole life. There were 3-4 people in the room prepping me, for the most part I had no idea what they were all doing or saying to me. But I do remember them marking the right side of my neck with an arrow pointing up to my head in black permanent marker and then them telling me they would be putting a cannula into my right hand, then once done they would put me to sleep and put something called and <a href="https://www.edwards.com/gb/devices/Catheters/Arterial-Cannulae">Arterial cannula</a> directly into my artery in my right arm. They also put 2 other cannulas into the top of each of my feet. I’m very glad they did all this while I was asleep as I hate cannulas at the best of times! The last three sounded deeply unpleasant.</p>
<p>So that was that, cannula in the right hand, they said they were injecting me with anaesthetic and it would feel cold running up my arm and that I may have a strange taste in my mouth. I felt the anaesthetic reach the right side of my neck and I passed out.</p>
<p>During the time I was passed out the team prepared me for surgery. I was moved onto my left hand side and my head was placed in a metal contraption with 4 very sharp points sticking into my skin; these were to hold my head in place for the operation. The team cut the skin on the top of my head and peeled and folded it forward to expose my skull. At this point they then used some sort of medical saw to cut through my skull and expose my brain (and Gary) The incision started at the front of my skull just after my hairline and travelled backwards right down the centre before turning to the right and coming back to just before my right temple in a large horseshoe shape. They’d basically had to remove the right hand side of my skull due to the sheer size of Gary (the prick). Thank goodness for anaesthetic and anesthetists is what I say!</p>
<p>Once all this was done they reduced the anaesthetic and used other drugs to bring me round. I remember waking up lying on my left hand side, my head was already open and I couldn’t move, there was a large transparent plastic cover in my peripheral vision to the right just above me (like the plastic covers you see on children’s buggies to protect them from the wind and rain). There were 2 people in front of me and a television screen just behind them. One person was the consultant anesthetist just watching proceedings, as I guess he’d done most of his job and was just monitoring the situation. The other person was a lady. She was the Neurophysiologist looking after me, her job was to be the go-between between me as a patient and the Neurosurgeon. She’d ask me questions and update him on how I was reacting to those questions.</p>
<p>Then the last thing in front of me was the TV. It had live pictures of the inside of my head, now they weren’t gory pictures with lots of blood and bone etc. They were the 3D scans of my head taken from my last MRI scan. The crazy thing I remember is they updated in real-time, I could see exactly where the Neurosurgeon was putting his tools. There was no pain while he was doing this but I could feel a distinct “pressure” inside my head that I’d never felt before, so I knew he was there.</p>
<p>I remember finding the real time 3D imaging fascinating. When they gave me the chance to ask them questions I remember asking them how it all worked. I was thinking it was some sort of <a href="https://en.wikipedia.org/wiki/Haptic_technology">haptic technology</a> driven tool but it turns out it was a lot more clever than that. The team had placed 3-4 sensors on the outside of my skull which could detect the “marking tool” the surgeon was using inside my skull. By monitoring the input from these sensors they could get a computer to update the screen in real-time, even for a person who works in technology on a day-to-day basis this blows my mind!</p>
<p>So what exactly were they marking in my head? Well it turns out this is where the <a href="#mri--pet-scan-day---wednesday-8th-september">PET scan</a> that I’d had on Wednesday comes in. The feasibility study in theory should be able to be used to identify the different cancer grades within the tumour, something that an MRI / CT scan on it’s own can’t show. You can see the results of my PET scan below:</p>
<figure class="figure">
<a href="/images/bugger/pet-scan.jpg">
<picture>
<source srcset="/images/bugger/pet-scan.avif 2272w" type="image/avif" />
<source srcset="/images/bugger/pet-scan.jxl 2272w" type="image/jxl" />
<img loading="lazy" width="2272" height="1303" class="figure__image" src="/images/bugger/pet-scan.jpg" alt="PET Scan of the tumour." />
</picture>
</a>
<figcaption class="figure__caption">PET scan results for Gary.</figcaption>
</figure>
<p>Now this is an ongoing study and I’m one of around 30 patients in the trial. The final results of the study won’t be known for many years. The above image is the result of the hour-long scan I had to endure. The radioactive isotope was absorbed by Gary, then depending on the grade, different parts emit radiation at different rates. This Is then picked up by the CT scanner resulting in the image above, hence showing Gary’s internal structure. My assumption is that the red areas are the higher grade (e.g. grade 3), through to green / yellow being lower (e.g. grade 2). But this won’t be known and confirmed until the study concludes in a few years.</p>
<p>The reason I’m telling you this is because when I woke up I heard the team were having a conversation where they were talking about colours and numbers. As it turns out the team were talking about these scans. They’d identified certain areas of Gary that they would like to biopsy for further analysis, before moving onto removing Gary completely. I think because Gary was so big there was a treasure trove of data for the study! I heard from the team post-surgery that they removed 8 areas of Gary for further analysis. They would have liked to have taken more but the Neurosurgeon put his foot down to say no more, as the procedure with only 8 added 1-1.5 hours more to the surgery time. One thing I remember finding amazing was how the whole team was working together. The Neurosurgeon was working inside my head but he obviously couldn’t see the real-time scans with the areas he was supposed to be removing, so the team were speaking to him telling him to move left / right / up down etc. Only the language they used was more medical than that, so using more medical references from the brain. It was incredible to see and hear.</p>
<p>I remember asking a few more questions about what was going on that the Neurophysiologist couldn’t answer and getting the response “that would be a question for the Neurosurgeon, but he’s a little busy at the moment!”. Then I think they put me to sleep again (probably to shut me up to be honest!). I’ve been told there were between 12 - 14 people in the operating theatre at the time but I only remember seeing 2 people and hearing the Neurosurgeon. The other folks must have been supporting the Neurosurgeon “above me”.</p>
<p>The next thing I remember is coming to, my eyes were closed so I couldn’t see anything but I could feel they were stapling my head back together. I felt no pain but I just remember the pressure of the staple gun reverberating through my body. I also remember thinking “that’s a lot of staples!”. If you’ve ever used a staple gun you will know that you have to put quite a lot of pressure on the handle before the staple is released. Well it sounded exactly like that, only into the top and side of my head around 95 times!</p>
<p>Annoyingly that’s all I really remember from the surgery. I estimate maybe 45 mins to an hour of the 7 hour operation. I’ve actually been quite disappointed that I don’t remember more of it and nothing more has come back to me over time. A couple of weeks after surgery I spoke to one of the Neurosurgeons who was also in the operation helping, about this. And she said that this was intentional. They constantly increase and decrease the drugs throughout the surgery as there are certain unpleasant things they don’t want you to remember. So I’m thankful for that, but in another way I will always wonder what happened in those 6+ hours of my life I can’t remember!</p>
<h2 id="recovery-starts---friday-10th-september-evening--night">Recovery starts - Friday 10th September (evening & night)</h2>
<p>The next thing I remember is waking up on the hospital ward with the absolute worst headache I’ve ever had, not really surprising when they’ve just removed approximately 15% of my brain matter! I could barely move and had to sleep upright due to the pressure lying flat would cause.</p>
<p>I’m so glad they put me to sleep before inserting the Arterial Cannula, as the bruise I had on my right arm was huge! It must have been quite a struggle to get it in there!</p>
<p>Now I have to be open and honest. This was the worst night’s sleep I think I’ve ever had, hence why I was grateful I slept at home the night before. I’m not a fan of hospitals anyway, but this night will stay with me forever.</p>
<p>Due to the risk of blood clots I had my DVT leggings on, but as an extra “brucie bonus”, I also had a pair of leggings that inflated & deflated every 10-30 seconds. The only way I can describe it is when you go to have your blood pressure checked and the strap on your arm inflates then deflates. Well imagine that on the total length of both calves. Inflate / deflate all night long every 10-30 seconds!</p>
<p>I also found I couldn’t really move both because of the leggings and the pain it caused in my head. I found my legs were roasting hot but my top half was cold because I couldn’t pull the blanket up over myself. What made things worse was I couldn’t find the call button to ask for help. I must have spent 20-30 mins looking for it before I eventually found it (it may not have been that long but it felt like it!).</p>
<p>Just to make things worse the patient opposite me who I’d spoken to before surgery was being violently sick all night. I think he must have had a bad reaction to the anaesthetic.
So yes, all in all a terrible night’s sleep 0/10 would not recommend!</p>
<h2 id="lets-get-the-hell-out-of-here---saturday-11th-september">Let’s get the hell out of here - Saturday 11th September</h2>
<p>After that one night in hospital I was absolutely determined to not have to stay another. So once it was light the nurses brought around food that I couldn’t eat. Everything tasted strange from the anaesthetic. I eventually managed to eat dessert, which was a rhubarb crumble with custard. The inflating leggings were taken off so I could finally move my legs. At that point I realised I had a much bigger scar than I expected so I wanted to go into the toilet to take a look. The toilet was only a few meters away but it felt like miles. I asked for a walking frame so I could get there myself but I had to be escorted for the first few times. Looking in the mirror was a weird experience. I couldn’t see the scar at the moment as it was covered in a blue gause, but it was a hell of a lot bigger than I expected. I’m not sure why I thought it would be smaller looking at the size of Gary. How else were they going to extract the prick?!</p>
<p>At the same time as looking at my head for the first time, I managed to brush my teeth! But no matter how much I tried I couldn’t get the weird taste of anaesthetic out of my mouth. Eventually after what seemed to take an age to do these small things I was escorted back to my bed, mostly under my own steam (with a walker).</p>
<p>Next came something that would be a thorn in my side for the next 3-4 weeks. Lying down or sitting up caused terrible pain in my head. This is because, unlike pretty much everyone but medical folks would know, when you get up / sit up / lie down, the <a href="https://en.wikipedia.org/wiki/Intracranial_pressure">intracranial pressure</a> inside your skull changes. Usually you’d never notice this, but when someone has just removed a huge chunk of skull and stapled it back on, you suddenly feel it!</p>
<p>It seemed to take me ages to lie down on the bed but I eventually managed it and got comfortable. But I was determined not to have to stay another night in the hospital so I decided I needed to get moving. Over the next few hours I kept getting up and down again and used the walker to move around the ward. Once I was steady on my feet the nursing staff were happy to let me walk on my own (it was more of a shuffle really!).</p>
<p>At 12pm the nurse came over and said my wife (Claire) would be visiting in the next couple of hours, this perked me up a bit. It would be good to see a familiar face after what had felt like weeks since the previous morning! Claire was very surprised when the nurse mentioned that I was already up and walking about the ward.</p>
<p>2pm came and my wife brought in some M&S chocolates and she got to see my humongous scar. Thankfully I’d had no memory loss, but I was finding speech and concentration very difficult. I don’t think we said much. I think I was mainly staring off into space or at the ceiling wishing the pain in my head would go away. The only painkillers I could have were paracetamol, none of the good stuff I’m afraid! Thankfully they took the edge off the pain. After an hour it was time for my wife to leave. She said she would be back to see me again tomorrow. But I had other plans.</p>
<p>Claire phoned my parents after leaving the hospital and pretty much said there is no way I’m going to be released from hospital tonight and it will most likely be Monday. So my parents stood down from coming to pick me up since they were a 1.5 hour car journey away. But during this time one of the senior doctors came to see me. He said that as long as I’m up and about moving we could try and get a CT scan sorted to check everything in my head was okay and try to get me discharged. I jumped on that in a flash!</p>
<p>I had to wait an hour or so for the CT scan then the porter came to wheel me across to the other side of the hospital. The CT scan took 10-15 mins or so then they took me back up to the ward. It was around 30-40 mins later that the same doctor came to see me and said everything looked great, so if I wanted to he’d be happy to discharge me. I obviously said yes, and he went off to pull together my discharge paperwork (which would take a couple of hours). I immediately tried to phone Claire but I had a slight weakness in my left hand, and I also found that the phone didn’t make as much sense as it had done the day before. Things hidden behind icons were difficult to find. I found this very worrying considering how easy it was just a single day before. But I eventually managed to work out how to use the phone and rang her. At first she couldn’t believe they were discharging me as I was still slurring my speech down the phone. Somehow I managed to get her to believe me with the help of one of the nurses who overheard via speakerphone. So it was then arranged that my parents would travel down to pick me up from the hospital and take me home. This eventually happened around 10pm. So from admission through surgery to discharge I’d been in hospital for 36 hours! That’s pretty crazy when you think about it. 15% less brain and out in a day and a half! To give you an idea of how busy the hospital was at the time, I heard the nurses discussing the next patient who was going to take up my bed once I’d been discharged! The nurses really are unsung heroes, day in, day out working with sick patients, and it isn’t easy work at all!</p>
<p>I don’t remember any of the journey home, but I do remember that the car was really low and difficult to get into especially given all the staples in my head! I don’t think my parents had a very good night’s sleep that night. Mine wasn’t perfect but it was 1000% better than the night before. I still had to sleep at an angle for another few days which was uncomfortable but atleast I wasn’t in hospital!</p>
<h2 id="how-does-the-tv-work---sunday-12th-september">How does the TV work? - Sunday 12th September</h2>
<p>I woke up early as I was in pain and uncomfortable. The F1 Italian Sprint race had happened on Friday during my operation so I wanted to catch-up on what had happened. My Dad had recorded it on Sky+ so before they woke up I decided to go downstairs and try to watch it. Unfortunately, I couldn’t work out how to use the Sky box. I was able to turn it on and find the recordings but when I played them it wasn’t the Formula 1, it was the Formula 2. This totally confused me because the thumbnail preview was showing a Formula 1 car. I tried multiple times to get to the F1, but had no luck. Eventually I gave up and used my phone to watch the highlights on YouTube. This was the start of an ongoing theme over the next few days. Things that had seemed so simple and obvious only 2 days before I was suddenly struggling with. I had the same issue with the Netflix User Interface. I found it too busy and confusing to use. For a once technically proficient person I found this genuinely scary. Thoughts of “how exactly am I going to do my job” if I’m struggling with these simple things! Not fun, and quite a contrast to me only 2 days before. It really did give me some insight into the importance of making things simple and understandable for users that are less technically proficient. If only every web development team could have similar brain surgery. I think we’d have a much more user-friendly internet if so.</p>
<h2 id="is-my-head-leaking---monday-13th-september">Is my head leaking? - Monday 13th September</h2>
<p>Odd title I know, but I woke up on Monday and found spots of red transparent liquid on the white pillowcase I’d been sleeping on. Further to that I was certain I could feel “movement” underneath the blue gauze which covered the staples in my head. When I felt something drip onto the bridge of my nose, I immediately got in contact with the Neuro-Oncology team via email to let them know. Within 5 mins they’d got back to me via email and a phone call. They asked me to come back to the hospital as quickly as possible. Of course I was 1.5 hours away at my parents so it wasn’t as simple as that. So I quickly packed a bag of clothes just in case I needed to stop overnight and we headed back along the motorway to the hospital.</p>
<p>Once we got there I waited with Claire who’d met us there for 5-10 mins and one of the Junior doctors took us into a side room and started to remove the staples from my gauze so they could get a better look at my head. She explained that if it was that Cerebrospinal Fluid (CSF) or “brain juice” (as I liked to call it) was leaking from the wound it also means that things can get into the wound / my skull. If this happens it can cause an infection, leading to a whole host of other issues that may even require more surgery. So it’s best to get it checked out. The Junior doctor couldn’t find any obvious leaks so she consulted one of her more senior colleagues. He came to see me and also couldn’t find any obvious leaks. So he decided to do a very scientific test to make sure. He placed a white absorbent pad on my head (like a kitchen towel) and then had me cough and lie down on the bed. This increases the intracranial pressure inside the skull (I know because I could feel it!) and if there’s a leak CSF would come out of the wound and be seen on the absorbent pad. Thankfully nothing was seen on the pad!</p>
<p>The senior doctor was certain the movement and liquid I’d felt / seen was the petroleum jelly (think Vaseline) used to keep the wound moist and clean. My warm head was heating the jelly, turning it to liquid and it felt like water (because I couldn’t see it to verify it wasn’t). He did leave me with a nice vision: Saying that if I do have a CSF leak it would literally be pouring down my face, and if this were to happen phone the emergency services immediately. I’m pretty sure I’d have done that anyway, but it’s always good to know I guess!</p>
<p>With the panic over I headed back to my parents to continue my recovery, with hopefully less drama moving forwards!</p>
<h2 id="general-struggles---tuesday-14th-september">General struggles - Tuesday 14th September</h2>
<p>So I’m not going to give a day by day update on what happened as it’s very boring and tedious. It felt like a long time for me because I was obviously living it, so to write about it would be even worse! But here’s a summary of some of my general post-surgery struggles I had.</p>
<p>The DVT leggings were terrible to wear. I hate wearing socks to bed and now I had to wear these really tight leggings over my lower legs for a month! They were a pain to get off and it took help from both of my parents to get them back on. I guess it’s a small price to pay compared to fatal blood clots!</p>
<p>As mentioned before, sitting up and lying down was painful. Basically going from 0 to 90 degrees and vice-versa caused a terrible pain in my head, so I tried to avoid moving, but you’d be surprised how often you make these movements going about your general day-to-day life! This was the same for coughing, laughing and sneezing, all these increase the intracranial pressure in the skull. Everytime i sneezed it felt like the top of my head was about to split open!</p>
<p>Related to the above issue, lying down and getting into bed was a chore! I remember having to get up in the night to go to the toilet, then getting back into the bedroom, and it literally took me 15-20 minutes to pluck up the courage to lie-down on my pillow to go back to sleep! I knew that it would only take 2 seconds to do it, but when I also knew it was going to hurt, my brain and body refused to move. I just ended up rocking backwards and forwards on the bed for minutes at a time, hoping the momentum would carry me forwards and I could then go back to sleep.</p>
<p>A strange one but due to the fact that I’d had open-head awake surgery, when they close your head it’s impossible to remove the air pocket that is leftover. So for 6-8 weeks I had a huge air pocket at the top of my skull. And everytime I moved my head I could feel this air pocket move around my head. It wasn’t uncomfortable, it was just weird. It was a similar sensation to someone lightly brushing the top of your head. There’s nothing that the doctors can do about this, it just takes time for your body to naturally replace and recycle the CSF around the brain every 6 weeks or so. So the air was gradually reabsorbed back into my bloodstream. But this sensation lasted 6-8 weeks after surgery, maybe even more.</p>
<p>Related to the air in my head, I could feel my nose randomly “creaking”. It’s hard to describe but it felt like small air bubbles at the top of my nose releasing. I think it may have been related to the CSF fluid redistributing itself and the pressure inside my head stabilising itself over time. This lasted 2-3 months after surgery.</p>
<p>I had to keep the 95 metal staples in my head for 2 weeks and in this time I kept having phantom movements on the outside of my head. Almost like my body was realising that there was a foreign body (or 95 of them) attached to the outside of my skull. This stopped as soon as the staples were removed and the wound started healing.</p>
<p>And finally I think I may have been suffering from something called <a href="https://www.healthline.com/health/abulia">Abulia</a>. This is described as “a state of diminished motivation”. Now this is a self-diagnosis not from the doctors, but from reading up on the symptoms it certainly seems like what I was suffering with, at least in the first 3-4 weeks anyway.</p>
<p>All in all I think I’ve got off quite lightly considering the size of the scar and the size of Gary. The struggles above weren’t pleasant but they were manageable. I’m very surprised how few headaches I’ve had. If I were to have guessed what the most likely result of major brain surgery would be it’d be headaches. But apparently not. At Least in my case anyway!</p>
<p>It’s also worth noting that the only painkiller advice I was given post-surgery was to take 2 paracetamol! So remember that the next time you have a headache. Paracetamol are so good they even work for folks who’ve had 15% of their brain matter removed!</p>
<h2 id="biopsy-results---tuesday-21st-september">Biopsy results - Tuesday 21st September</h2>
<p>As with most things happening in the NHS at the moment teams are extremely busy. So I wasn’t expecting anything from the hospital on this day until I received an email at 9am asking if I was free for a call with my Neurosurgeon + team at 12:30pm. I replied back and accepted, I mean It’s not like I had much else on! I wasn’t quite sure what the call was about so really wasn’t mentally prepared for it at all.</p>
<p>I jumped on the call with the team, my wife and parents were listening in too. My Neurosurgeon, never one to mince his words, immediately spoke and came out with the biopsy results of Gary, the now (mostly) evicted prick! He told me the team had managed to remove a whopping 95% of Gary, but the biopsy results had come back to say that some parts of him were Grade 3, rather than Grade 2. Grade 3 are called <a href="https://en.wikipedia.org/wiki/Anaplastic_oligodendroglioma">anaplastic oligodendrioglioma</a>, these are malignant (cancerous) and can be fast growing. And because of this I’d need to start a 6.5 week course of radiotherapy within the next month, followed almost immediately by a 6 cycle course of <a href="https://www.cancerresearchuk.org/about-cancer/cancer-in-general/treatment/cancer-drugs/drugs/pcv">PCV chemotherapy</a> which will last approximately 7 months. As I’d experienced previously with my Neurosurgeon there was no warning about this news, no “are you sitting down, brace yourself”. Just boom, you’ve got Grade 3 brain cancer! Well you may as well pull off the plaster quickly and get it over with I guess! In hindsight I was expecting it to be a multi-grade tumour even before they told me, I mean why would they have been wanting to biopsy multiple areas in the surgery if it had all been a single grade tumour?</p>
<p>After the initial shock, I managed to compose my thoughts and asked a few questions about the surgery itself. I asked if there’d been any nerve damage at all, as I had a slight weakness in my left hand. He looked slightly bemused by this and said no there’d been no nerve damage and the operation had gone very well. It turns out there’d been a slight miscommunication from what was said to Claire immediately after she got the phone call from the Neurosurgeon to say surgery had gone well and that I was in recovery.</p>
<p>I was led to believe that they stopped surgery because they had caused slight nerve damage. But this wasn’t the case. It was actually that if they went any deeper it <em>could have</em> caused nerve damage, so they stopped. I later found out that the team were using a technology during the operation called <a href="https://www.healthcareers.nhs.uk/explore-roles/healthcare-science/roles-healthcare-science/physiological-sciences/neurophysiology">Neurophysiology</a>. This involved strategically inserting 5-8 metal pins into the left hand side of my body and monitoring the signals from these pins as they fired electrical impulses into my brain. It was basically a type of automated testing to see when they were getting close to nerves that actually did something, rather than just prodding Gary the useless prick. So once the safest amount of Gary had been removed, the team decided I was getting very tired and I needed to save my strength for recovery. So that’s why the operation <em>only</em> lasted 7 hours!</p>
<figure class="figure">
<a href="/images/bugger/gary-before.jpg">
<picture>
<source srcset="/images/bugger/gary-before.avif 2272w" type="image/avif" />
<source srcset="/images/bugger/gary-before.jxl 2272w" type="image/jxl" />
<img loading="lazy" width="2272" height="1306" class="figure__image" src="/images/bugger/gary-before.jpg" alt="MRI scan of Gary before the operation." />
</picture>
</a>
<figcaption class="figure__caption">Gary before the operation highlighted in red.</figcaption>
</figure>
<figure class="figure">
<a href="/images/bugger/gary-after.jpg">
<picture>
<source srcset="/images/bugger/gary-after.avif 2272w" type="image/avif" />
<source srcset="/images/bugger/gary-after.jxl 2272w" type="image/jxl" />
<img loading="lazy" width="2272" height="1303" class="figure__image" src="/images/bugger/gary-after.jpg" alt="MRI scan of Gary after the operation." />
</picture>
</a>
<figcaption class="figure__caption">Gary after the operation highlighted in red.</figcaption>
</figure>
<p>In the above images you can see the before at the top then the after scan of my brain with Gary highlighted in red. As you can see he’s mostly gone apart from some left over in the deepest part of my brain. This leftover is what the Radiotherapy and Chemotherapy are going to be targeting as well as any residual cancer cells around the edges of the surgery.</p>
<figure class="figure">
<a href="/images/bugger/red-brain-compared.jpg">
<picture>
<source srcset="/images/bugger/red-brain-compared.avif 2263w" type="image/avif" />
<source srcset="/images/bugger/red-brain-compared.jxl 2263w" type="image/jxl" />
<img loading="lazy" width="2263" height="1138" class="figure__image" src="/images/bugger/red-brain-compared.jpg" alt="Comparing imaging before / after for my brain" />
</picture>
</a>
<figcaption class="figure__caption">Image of my brain before (left) and after (right).</figcaption>
</figure>
<p>Now a question I’ve been asked many times off the back of the quite frankly incredible image above is “what happens with the hole moving forwards?”. Well this is a question I asked back when I was first diagnosed and they described the operation to me. The hole will now alway be there. Brain cells don’t grow back. Over time it fills with CSF, but it will always be there. And a fascinating observation I’ve noticed since the operation is I can <strong>hear</strong> the cavity where Gary used to reside in my head. This is especially true when I’m in the shower and I run the water over my head. The right hand side of my head sounds hollow compared to the left! I like to compare it to looking for a stud wall behind some plasterboard. As you tap on the plaster the sound changes from hollow to dense. It actually took me a while to work this out and what was different when I was having a shower in the mornings. It’s a strange feeling to know there’s now just a big hole in my head!</p>
<p>After the appointment I didn’t have long to wait as my initial appointment with the Oncology team to discuss the radiotherapy as it was set for the following day. No time to waste with Grade 3 brain cancer I guess!</p>
<h2 id="further-treatment-appointment---wednesday-22st-september">Further treatment appointment - Wednesday 22st September</h2>
<p>The whirlwind that my life had become continued with a meeting at a new hospital and a whole new team of experts. The meeting was to discuss the post-surgery treatment I was signing up for. This meeting was mainly about radiotherapy, with the chemotherapy discussion coming at a later date.</p>
<p>There were 2 oncologists and a number of nurses in the meeting as well as my wife. I’d made the decision that whenever something important was being discussed I should always have company as I knew for a fact I wouldn’t remember it all, since it had only been 12 days since my surgery! There was no chance of me remembering much!</p>
<p>The meeting seemed to last ages and there was so much information being discussed I found it hard to concentrate on what was being said, so I’m very glad Claire was there with me to take notes and remember everything.</p>
<p>As well as introducing the timeline for the radiotherapy all the wonderful side-effects were discussed too. And they always seem to save the best (and rarest) to last, which includes the fact that radiotherapy in some cases can lead to other forms of cancer. What’s better than having 1 type of brain cancer? Well 2 forms of brain cancer I guess!</p>
<p>Once all the difficult conversations were discussed, I signed on the dotted line to say I agreed with the treatment. I’d already decided this was the direction I wanted my treatment to go even before hearing all the side-effects so it wasn’t a hard decision to make.</p>
<p>With a room full of doctors and nurses I decided to ask the question if it would be possible to take the staples out of my head. It was coming up to 2 weeks since surgery and this was close to the date they had said, so I may as well have it done while I was there anyway.</p>
<p>I felt quite sorry for the nurse when she saw the sheer number of staples in my head. I wouldn’t even know where to start. The technique involves taking every other staple out first, just in case the wound hadn’t healed. I guess they don’t want your head to suddenly open up again, and every other staple allows for some support while assessing the wound. Removing the staples wasn’t as uncomfortable as you’d expect. There were a few minor sharp pains with a few of the staples, but most of them were pain free. Because there were 95 of them it seemed to take ages!</p>
<p>Once the staples were out I was done for the day and I was travelling back home to be with my wife and children. I also managed to wash my hair / head for the first time in just under 2 weeks. It was the most amazing shower and I actually felt a little more human after it!</p>
<p>My kids were at school when I got back, and I needed to sleep for a few hours. This was the first time they’d seen me since before the surgery so it was going to be interesting to see how they would react to the huge scar on my head. My youngest son wasn’t phased at all but as soon as my eldest saw me I could tell he wasn’t comfortable with my new Frankenstein’s monster look. We tried not to make a big deal of it but I ended up having to wear a hat around the house for a few weeks while my head healed to stop him freaking out.</p>
<h3 id="prognosis">Prognosis</h3>
<p>So jumping back to the meeting with the team they went over the scans and some of how well the surgery had gone and then got down to the nerve-racking part, which is the future prognosis. They could only give me timescales for the “average” patient in my position. It was mentioned that this isn’t a cure and it’s likely to always be there.</p>
<p>The timescales they gave me are below:</p>
<ul>
<li>Do nothing - the average patient can expect to live <strong>7 years</strong></li>
<li>Radiotherapy followed closely by chemotherapy - the average patient can expect to live <strong>14 years</strong></li>
</ul>
<p>So here’s hoping I’m the exception to the rule or not the “average” patient. Else I certainly won’t need to worry about retirement plans (that’s probably a blessing actually)! Given the retirement age for men is 68 in the UK, I’ll fall some 15 years short. But a lot can happen in 14 years, you never know with further funding and research more options could be available by then!</p>
<h2 id="making-my-mask----friday-1st-october">Making my mask - Friday 1st October</h2>
<p>Because Radiotherapy is a very targeted treatment, they need to make sure the treatment area is accurate for every session, and this involves me not moving my head at all for around 6-8 minutes while the treatment happens. This of course is impossible without aid, so they make a fitted mask that you wear throughout the treatment, that you can then take home should you wish to at the end!</p>
<p>The mask is made by heating up a rubber mesh to around 70 degrees centigrade in what looks like a pizza oven. While you are lying on the “couch” they quickly pull the heated mesh over your face / head and manipulate it a little to make sure it fits snugly. Then you have to wait 20 minutes underneath it while it cools, before it can then be taken off.</p>
<p>It’s a very strange sensation when they first pull it over your face. It’s like someone is pulling a plastic bag over your head and for the first second or so there’s initial panic in that you think you can’t breathe. But once you breathe through your nose it’s actually quite pleasant and warm. You also need to close your eyes so as to not pull out your eyelashes when they remove it. A side effect of this means that during the treatment itself you have no idea what is happening around you.</p>
<figure class="figure">
<a href="/images/bugger/finished-mask.jpg">
<picture>
<source srcset="/images/bugger/finished-mask.avif 1200w" type="image/avif" />
<source srcset="/images/bugger/finished-mask.jxl 1200w" type="image/jxl" />
<img loading="lazy" width="1200" height="567" class="figure__image" src="/images/bugger/finished-mask.jpg" alt="Picture of my finished mask!" />
</picture>
</a>
<figcaption class="figure__caption">A picture of the finished mask!</figcaption>
</figure>
<p>It’s also worth noting that when I say you lie on a “couch’’ it’s not like something you sit on to watch TV. It’s actually a flat carbon fiber board that isn’t built for comfort. It’s built for accuracy.</p>
<h2 id="radiotherapy-starts---tuesday-19th-october">Radiotherapy Starts - Tuesday 19th October</h2>
<p>The radiotherapy happened over 6.5 weeks, with 33 sessions in total. I needed to go into the hospital everyday apart from weekends. The actual treatment only took 8-9 minutes once I was in the room. It was the travelling there and back (~45 mins each way), plus there was sometimes a wait for the machine to be ready, as there were sometimes delays with other patients. The worst day I had was one particular Friday where I left the house at 8am and used the NHS hospital transport. My treatment was delayed by 1.5 hours, so I missed my transport slot to go home. I eventually got back at 14:30 and was absolutely shattered.</p>
<p>I’d got into the routine of having the treatment, coming home, eating, then going to bed for 2-3 hours. Because of this I was so tired, there was no way I could do anything really for 6.5 weeks. The effect of the treatment was cumulative, so as the weeks went on I got more and more tired.</p>
<p>I’m not going to cover each day, but will give a week by week blow of the whole experience.</p>
<h3 id="week-1-19th---22nd-october">Week 1: 19th - 22nd October</h3>
<p>This week was mainly getting used to the schedule of how it all worked, signing in everyday, waiting in a particular area, hoping that there wasn’t a delay on my particular machine etc. It’s incredible the cognitive load that the “unknown” puts on you. But once into the routine it was fine. The only real side effects I had in the first week was tiredness, and getting used to staying perfectly still when I had the mask on. It was also trying to get used to the machines and what exactly they were doing. As mentioned earlier, My eyes were shut for the whole process so I had no idea what exactly was happening around me for 8-9 minutes, I only had the sound of the machine to rely on. For my first couple of sessions it felt like the whole machine was rocking backwards and forwards because I had no point of reference. With my eyes shut and touching nothing but the “couch”, as soon as your brain thinks you are moving it is very hard to stop it from thinking it! Trust me I tried multiple times! I’m just very glad I don’t suffer from claustrophobia because if I did the whole 6.5 weeks would be a nightmare as that mask was very tight! Sometimes so tight it left an mesh imprint on my nose and forehead.</p>
<h3 id="week-2-25th---29th-october">Week 2: 25th - 29th October</h3>
<p>Week 2 was mainly travelling in and out via hospital transport so the days were long and it very much started to feel like “groundhog day”. The funny thing about Hospital transport is the drivers are all volunteers, some were very chatty, others wouldn’t say a word for the whole journey. I often found the drivers were chatty on the journey back, when I was tired after hours of being at the hospital. Small talk is difficult at the best of times, but 6 weeks after major surgery and radiotherapy it is a struggle! The main side effect from this week was I noticed I’d started to get a distinctive tan on my forehead from the radiation. It was like going on holiday without all the fun.</p>
<h3 id="week-3-1st---5th-november">Week 3: 1st - 5th November</h3>
<p>Week 3 was a tough week. The whole process was getting very repetitive and I was only just getting to the half-way point! This is also the point at which I noticed my hair was starting to fall out.</p>
<p>I was having a shower one morning and realised there was hair in my mouth. I quickly spat it out then a few seconds later realised there was hair in my mouth again. It was only when I washed my hair I noticed whole clumps of hair were coming out from the inside of my scar. I’d been expecting this as they’d warned me about it, but in the back of my mind I thought it may not happen to me. So it’s still quite a shock when it does happen!</p>
<h3 id="week-4-8th---12th-november">Week 4: 8th - 12th November</h3>
<p>Groundhog day continues and I notice more of my hair is falling out, this time from the other side of my head. I guess because of the geometry of how they are targeting Gary, they are hitting it from multiple angles. So I ended up having hair at the back and sides of my head and very little in the middle, with a small patch of hair right at the front of my head. It looked ridiculous so I decided to shave my head completely. This completed my full on “cancer look”, and it certainly shows off my scar a lot more!</p>
<p>The one thing I notice is how much colder my head actually is! You wouldn’t have thought such little hair would make a difference, but it really does! Thankfully I have a few hats that I wore both inside and outside the house.</p>
<h3 id="week-5-15th---19th-november">Week 5: 15th - 19th November</h3>
<p>At this point I’m well and truly over the whole process. The journey in and the appointment takes up the whole morning, then I’m sleeping in the afternoon. So by the time I wake up it is already getting dark. So the day is gone and I’ve got nothing to show for it. So incredibly frustrating!</p>
<h3 id="week-6-22nd---26th-november">Week 6: 22nd - 26th November</h3>
<p>Thankfully getting closer to the end of the whole process now, but I’m starting to feel incredibly drained. It got so bad at the end of the week that on Friday Claire had to phone the emergency number as I felt like I was about to have a seizure. This is something they had warned us about but I hadn’t had any seizure-like symptoms since before surgery. After lying down in a dark room for an hour or so and taking some extra medication that the oncall oncologist had recommended I felt a little better. But it felt depressing, as it was almost like I’d regressed weeks rather than moved forwards.</p>
<h3 id="week-7-29th-november---2nd-december">Week 7: 29th November - 2nd December</h3>
<p>The final week! In the weekly Tuesday consultation with the consultant / senior nurse, it was decided that I was going to be kept on stronger seizure medication in the evenings for the foreseeable future. Thankfully I haven’t had any huge side effects on this drug so I didn’t mind. As long as it keeps the seizures away!</p>
<h2 id="radiotherapy-ends---thursday-2nd-december">Radiotherapy Ends - Thursday 2nd December</h2>
<p>In my final session on the Thursday I took a whole bag full of chocolates in for the nurses and reception staff as they’d all been so helpful and friendly for the past 6 weeks. They seemed very happy with them. I guess they must get it quite a lot. Since it was my final session I asked if I could film my radiotherapy session as I was curious as to what exactly the machine was doing, having had no idea for the past 32 sessions. Thankfully they allowed it.</p>
<figure class="figure">
<a href="/images/bugger/me-on-the-machine.jpg">
<picture>
<source srcset="/images/bugger/me-on-the-machine.avif 2280w" type="image/avif" />
<source srcset="/images/bugger/me-on-the-machine.jxl 2280w" type="image/jxl" />
<img loading="lazy" width="2280" height="1080" class="figure__image" src="/images/bugger/me-on-the-machine.jpg" alt="Picture of me lying on the machine." />
</picture>
</a>
<figcaption class="figure__caption">A picture of of me on the machine for my final session!</figcaption>
</figure>
<p>Once the machine passed over me for the final time I remember clenching both my fists and saying a little “yes!” to myself, 6.5 weeks and 33 sessions over! And the final time I’d need to put on the uncomfortable mask! You get the option at the end about if you want to keep the mask and take it home. I decided I wanted to keep it. I’m not sure why. Maybe I can pass it onto my kids and grandkids in the future? Maybe it will become a family heirloom that they can one day take onto the Antiques Roadshow! But for now it sits up in the loft so as not to scare the hell out of them!</p>
<figure class="figure">
<a href="/images/bugger/mask-party-hat.jpg">
<picture>
<source srcset="/images/bugger/mask-party-hat.avif 1200w" type="image/avif" />
<source srcset="/images/bugger/mask-party-hat.jxl 1200w" type="image/jxl" />
<img loading="lazy" width="1200" height="900" class="figure__image" src="/images/bugger/mask-party-hat.jpg" alt="Picture of the mask with a party hat on" />
</picture>
</a>
<figcaption class="figure__caption">A picture of the mask with a party hat on to celebrate the final session.</figcaption>
</figure>
<p>We had a little celebration as a family, a mini post-radiotherapy party with party plates, hats and balloons too! No jelly and ice cream though. Probably a good idea as I’m trying to cut down on sugar!</p>
<h2 id="whats-next---friday-3rd-december">What’s next? - Friday 3rd December</h2>
<p>After 6.5 weeks of going to the hospital everyday, Friday was unusual. I didn’t need to be anywhere or subject my head to both an internal and external suntan! I think I spent most the day sleeping trying to recover from the cumulative effects of the radiotherapy then had a takeaway (Indian) in the evening. All in all the treatment had been long and repetitive, but the side effects I had were all manageable.</p>
<p>The one thing I would say about the whole experience is it’s quite lonely. That’s not to say I’m not surrounded by friends,family and colleagues (past and present) but at the end of the day it’s me who is waiting around and then me lying in the machine(s) for the treatment and you can only ever do that alone. It certainly doesn’t help at the moment that Covid restrictions don’t allow <em>anyone</em> to be with you in the hospital. So if you are waiting 1.5 hours for a delayed machine. You are waiting alone.</p>
<p>But I’d like to say a huge thank you to all my friends / family and the wonderful NHS Transport service for getting me too and from each of my sessions over the 6.5 weeks.</p>
<p>So what next? Well I have approximately 6 weeks off over Christmas then should start 4-6 cycles of PCV chemotherapy some time mid-January. All being well this will last approximately 7 months. Well that’s a lot of 2022 planned for then! I’m hoping that around March they will be able to do another MRI scan of my brain and tell me what difference the radiotherapy made to Gary’s leftovers. Apparently it takes around 3 months for the brain to recover and repair after radiotherapy, and until it does they can’t see on the scans if it has made a difference.</p>
<p>So watch this space for more updates in 2022, where the remaining 5% of Gary is hopefully evicted from my head!</p>
<figure class="figure">
<a href="/images/bugger/me-walking-out.jpg">
<picture>
<source srcset="/images/bugger/me-walking-out.avif 1200w" type="image/avif" />
<source srcset="/images/bugger/me-walking-out.jxl 1200w" type="image/jxl" />
<img loading="lazy" width="1200" height="993" class="figure__image" src="/images/bugger/me-walking-out.jpg" alt="Picture of me walking out of the radiotherapy ward for the final time!" />
</picture>
</a>
<figcaption class="figure__caption">A picture of me walking out of the radiotherapy ward for the final time!</figcaption>
</figure>
<h2 id="oncology-meeting---wednesday-29th-december">Oncology Meeting - Wednesday 29th December</h2>
<p>So since my last update at the start of December, not much has really happened. I’ve had 5-6 weeks off, had a good Christmas with family and tried not to think too much about 2022 (although I must admit it’s almost impossible). As mentioned before, the plan moving forwards was radiotherapy, followed almost immediately by chemotherapy. At my initial follow-up meeting <a href="#further-treatment-appointment---wednesday-22st-september">back in September</a> very little was mentioned about the chemotherapy. This was intentional by the team, since getting my head around radiotherapy was enough for me at that point! So I’ve only had a few basic details about it all. This meeting was to tell me a lot more.</p>
<p>As with most of my meetings, it was a video call, it included two oncologists and a senior nurse. They went through a few of the details about the type of chemo I’m going to be having (it is called <a href="https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv">PCV</a>), PCV stands for:</p>
<ul>
<li>P – Procarbazine</li>
<li>C – Lomustine (CCNU)</li>
<li>V – Vincristine</li>
</ul>
<p>These are the three types of drugs I’m going to be taking for the next few months. Vincristine is taken intravenously, the other two drugs are prescribed to me and taken in pill form at home.</p>
<p>I’m going to be taking these drugs in what are called cycles. Each cycle is 42 days long, and I will have up to 6 cycles (assuming my body allows for it). A cycle consists of:</p>
<ul>
<li>Day 1 - Go to the hospital and sit while they give me Vincristine via a drip.</li>
<li>Day 1 to 10 - take the Lomustine and Procarbazine every day at the same time each day</li>
<li>Day 11 to 42 - This is a rest period for my body to recover from the chemotherapy drugs.</li>
<li>Repeat this process up to 6 times if my body allows for it.</li>
</ul>
<p>Now, the difference between the radiotherapy and chemotherapy is that radio is very targeted (hence my ‘sunburnt’ head!). But chemotherapy isn’t targeted at all, it is given to me and can have an impact on all the cells in my body. If you take a <a href="https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv">look at the potential side effects</a> this is the reason why there can be so many!</p>
<p>A big negative of chemotherapy is it can have quite a big impact on my immune system. It reduces the white blood cell count in my body. These cells are used to combat infections. So the less white blood cells you have, the more susceptible to other illness’. This is what I mean when I mention “if my body allows for it”. At the start of each cycle, the hospital looks at my white blood cell count to determine if I am healthy and able to continue with the next cycle. If not then they may lower the drug dosages or if it’s terrible, pause the chemotherapy completely. Each and every person’s body reacts differently to the drugs, so I won’t know until I’m into the first couple of cycles as to how my body reacts. So it’s another journey into the unknown!</p>
<p>Six cycles at 42 days each, so I’m looking at around 7 months of chemotherapy! That’s quite a long time to try and not get sick! Especially during a global pandemic! Speaking of which, I asked about Covid and If I do end up catching it the NHS are able to give me <a href="https://www.theguardian.com/world/2021/sep/18/covid-antibody-drug-ronapreve-to-be-given-to-vulnerable-nhs-patients">special drugs</a> to help my body combat the virus.</p>
<p>So it’s people in my position across the world who are in danger when a huge portion of a population refuse to take the vaccine. As if life isn’t bad enough when you have cancer, only to find going out people around you could make you even sicker! So yes, for the next 7 months I’m generally going to be avoiding people I don’t know, and asking friends and family I see to take a test before meeting with them. Not a great position to be in! I’ll get off my soapbox now…</p>
<p>The only disappointing part of the meeting was they were unable to give me specific dates for my treatment or the hospital it would be at. This isn’t the fault of the doctors, it’s just a scheduling and administration bottleneck given the pressure the NHS is under at the moment. They said I’d be receiving a few phone calls in the coming days to confirm dates etc. Until then, we wait!</p>
<h2 id="houston-we-have-dates---thursday-30th-december-2021">Houston, we have dates! - Thursday 30th December 2021</h2>
<p>Well, thankfully I didn’t need to wait long for the dates. I received a few phone calls the day after the meeting:</p>
<ul>
<li>Monday 10th January - chemotherapy pre-admission</li>
<li>Tuesday 11th January - start of cycle 1</li>
</ul>
<p>In the previous meeting, they also mentioned that I’d need to have another MRI scan to see what effect the radio has had on the remaining 5% of Gary, but they said this could take a while to schedule, and I may have started chemo before it actually happens.</p>
<h2 id="death-to-2021---january-1st-2022">Death to 2021 - January 1st 2022</h2>
<p>As I said at the start of this very long blog post. Just when I thought 2020 was bad, 2021 comes along and proves me wrong! It has undoubtedly been one of the worst years of my life, but that being said I’ve achieved a lot (well up until August anyway!). I worked up until 2 weeks before surgery, so I managed to pack quite a bit into the first 8 months. My year in review:</p>
<ul>
<li>January: Spoke at <a href="https://archive.fosdem.org/2021/">FOSDEM21</a></li>
<li>February: Spoke at <a href="https://ldnwebperf.org/sessions/how-to-read-a-webpagetest-waterfall-chart/">London Web Performance</a></li>
<li>March: <a href="/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/">Cloudflare Worker experimentation blog posts</a></li>
<li>April: Diagnosed with a Grade 3 Brain tumour</li>
<li>May: Spoke at <a href="https://conferences.css-tricks.com/conferences/2021-lazyload-webperf-2021/">Web Directions Lazy Load</a></li>
<li>June: Lots of Hospital appointments</li>
<li>July: Missed the F1 at Silverstone due to the Covid risk before my surgery date.</li>
<li>August: Stopped work before surgery</li>
<li>September: Had awake open-head brain surgery</li>
<li>October: Recovery + Radiotherapy</li>
<li>November: Lots of radiotherapy</li>
<li>December: Turned 40 and got a Puppy</li>
</ul>
<p>Not bad, all things considered! Here’s hoping for a better 2022! Fingers crossed the final 5% of Gary gets evicted from my head!</p>
<h2 id="mri-scan-scheduled---thursday-6th-january-2022">MRI Scan scheduled - Thursday 6th January 2022</h2>
<p>So much for it taking a while to schedule the MRI! I received a phone call asking me to come in to hospital the next day (Friday) for my MRI scan. Not much notice, but the good thing (I hope) about this date is they should have the scans back for my pre-admission meeting, so you never know they may actually be able to give me an update on how the rest of Gary the prick is looking!</p>
<h2 id="mri-scan---friday-7th-january">MRI Scan - Friday 7th January</h2>
<p>You’d have thought by now that the thought of lying still in a tube doesn’t phase me, but it still does. There’s nothing particularly scary about it. It’s very loud and a little uncomfortable (and very boring). But other than the cannula for the contrast they inject, it’s not that unpleasant. Even so, I still get a little nervous before it happens. I think it’s because when you are in the machine alone with your thoughts, there’s no way to ignore the fact that you have cancer. It isn’t a bad dream that you are going to suddenly wake up from. It’s actually that you are lying in this machine (again) for 25 minutes!</p>
<p>Anyway, the scan was uneventful. Just another hurdle to jump over in the whole process!</p>
<h2 id="chemotherapy-pre-admission---monday-10th-january">Chemotherapy pre-admission - Monday 10th January</h2>
<p>The meeting was a bit of a disappointment. I was expecting there to be a bigger team to discuss what was going to be happening, but in the end it was “only a nurse” (her words, not mine!). I managed to get a few questions answered, but I have many more for the consultants / doctors.</p>
<p>Unfortunately, due to it being a specialist cancer centre, Claire was unable to be at the meeting with me. She had to sit in the car and listen in while I had her on speakerphone. This is due to the current Omicron covid variant restrictions. The team mentioned that if I’d started just 2 weeks earlier, it wouldn’t have been an issue. But they’ve had quite a few patients partners walk into the centre who knowingly (or unknowingly) were covid positive, which obviously puts all patients at risk. So now they <em>only</em> allow patients in for meetings and treatment. And as a patient, you have to bring a negative lateral flow test from that day with you before you are allowed to be admitted into the centre.</p>
<p>On a more positive note, I’ve got all the dates for my cycles. So if all goes to plan, and I manage all 6, my ‘last day’ will be on the 20th September. Although I have no idea how long chemotherapy drugs stay in your system, so they may linger for a little while after that??</p>
<h2 id="chemotherapy-cycle-1-start---tuesday-11th-january">Chemotherapy cycle 1 start - Tuesday 11th January</h2>
<p>I was incredibly nervous about the chemotherapy, I’m not 100% sure why, as the doctors had mentioned that it would likely not be as bad as the radiotherapy in my instance. I think it’s probably due to the longevity of the treatment. The surgery was scary but in all honesty if something went wrong I doubt I’d have known very much about it, I think it would have all just ended. Radiotherapy was localised long and repetitive, but once in the room it only took 5 minutes for the treatment, then you were done for the day. But with chemotherapy, I’d be taking the treatment “home with me”. With a <a href="https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv">tonne of possible side effects</a>, and a total of up to 6 cycles lasting up to 7 months, I’m actually going to feel the effects of this for a long time into 2022. There’s pretty much no avoiding getting ill in some way once started.</p>
<p>Thankfully, the journey to the cancer centre is nowhere near as tedious as the hospital trips for radiotherapy, and I only need to go once every 42 days, the rest of the cycle is at home. Once I’d chosen a chair to sit in, the nurse came over and checked I was who I said I was, then explained what was going to happen (since it was my first cycle).</p>
<p>I was given the <a href="https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/vincristine">vincristine</a> intravenously into my arm, then while that was happening I’d speak to the pharmacy team about the rest of my drugs. Unfortunately, the drip has to go into the back of the hand or wrist for this type of drug. This is because vincristine can cause damage to skin when outside of veins. The inside of my elbow (where’d I’d prefer it to be given) has a void behind the vein, so if there were a leak then the nurses wouldn’t know about it! Not ideal, but I can see why it is done! So in the end we managed to come to a compromise where I was cannulated on the side of my wrist behind my thumb.</p>
<p>Once the cannula was in my wrist, the drip only took 5 minutes to give me all the medicine it needed to. In this time, I spoke to the pharmacy team with Claire on the speakerphone, so she could listen in (she was sitting in the car due to covid restrictions). It was only then that I realised the sheer number of pills I was going to have to take at the start of each cycle. A minimum of 69 tablets over the first 10 days at different times of day, on top of the seizure tablets I already take! I’m very glad I don’t have problems taking tablets!</p>
<p>Once the drip had finished, that was it. I was sent home with a small green bag full of all the medication I’d need for the next 41 days. Before the process starts all over again!</p>
<p><strong>Side effects</strong>
It’s been just under a week into my first cycle as I’m writing this, and I have a pretty good idea as to how my body is reacting to it all. The first two days were pretty awful, very tired and lethargic and just generally not feeling 100%.</p>
<p>But It was into the third day that I started to feel the main side effect I’ve noticed. Eating my breakfast on the third day, the milk tasted metallic and my tongue / mouth were extremely sore. I’d love to say that this has improved, but it has actually got worse over the past days. Nothing I eat or drink tastes the same, and it has put me totally off food. It feels like I have a mouth full of ulcers. Not pleasant! Apparently this is a side effect of the vincristine so should hopefully diminish over time.</p>
<p>The only thing that has alleviated the pain is taking regular dosages of painkillers (more pills!), <a href="https://www.corsodyl.co.uk/products/corsodyl/mouthwash/">Corsodyl mouthwash</a>, and <a href="https://www.medicines.org.uk/emc/product/9257/smpc">Difflam mouthwash</a>.</p>
<p>If anyone is in the same position, here’s a list of the foods I’ve had so far that still taste like anything palatable:</p>
<ul>
<li>Rice Pudding</li>
<li>Custard</li>
<li>Baked Beans</li>
<li>French fries (no salt)</li>
<li>Steak pie</li>
<li>Boiled (or mashed) potatoes</li>
<li>Sausages</li>
<li>Gravy</li>
</ul>
<p>I’ll keep this list updated as I find more foods that taste okay, I hope there are more, else I’m in for a very boring 7 months of food!</p>
<p>On a positive note, I haven’t experienced any nausea, which is what I thought may happen. But then again, I <em>am</em> taking 3 types of anti-sickness tablets! So maybe they are doing their job at the moment and I have that to look forward to later in the cycle? I hope not!</p>
<p><strong>1p/19q Codeleted gene update</strong></p>
<p>I mentioned <a href="https://oncologypro.esmo.org/education-library/factsheets-on-biomarkers/1p-19q-co-deletion-in-glioma">1p/19q Codeleted gene</a>) before which will effect how effective the chemotherapy is against Gary, Thankfully it turns out that I <em>do</em> have this gene mutation, this is a good thing as it should mean that the chemotherapy should be more effective in my case! Yay for genetics!</p>
<h2 id="oncology-update---wednesday-19th-january">Oncology Update - Wednesday 19th January</h2>
<p>I was told this meeting was to discuss the results of my MRI scan, so I was quite nervous in the days leading up to it. But in the end it turned out to mainly be a catchup about how cycle 1 was going. I updated the team on the side effects I was experiencing and asked them a number of questions, one of these was how my brain was looking on the scan. Due to the fact that my brain is still healing from Decembers radiotherapy, they couldn’t tell me too much. But they did say that the tumour was looking “stable”, which I guess is good! They said that my next scan (date unknown) should tell us a lot more, as it will be past the 10-12 week cut-off for my brain healing from radiotherapy.</p>
<h2 id="chemotherapy-cycle-1-end---friday-21st-january">Chemotherapy cycle 1 end - Friday 21st January</h2>
<p>So technically this isn’t the end of the cycle, but it is the end of taking all the chemo drugs for cycle 1! 10 days and 70+ tablets later, It’s been a busy week, and I’ve learnt a lot that I can take into the next 5 cycles.</p>
<p>So what have I learned in cycle 1?</p>
<p>Well, first thing is to make sure you are taking the correct anti-sickness pills! I’d mistakenly been taking the wrong ones for the first few days. When you are feeling like crap and half-asleep at 6 in the morning, this was an easy mistake to make! They both look the same in colour and shape and come with some wonderful side effects! So yes, I won’t be making that mistake again!</p>
<p>Another thing is to start using both sets of mouthwash and dosing up on constant painkillers from day 1. The mistake I made was to wait for the side effects to become almost unbearable, before contacting the cancer triage number. Hitting it hard from day 1 will hopefully make the first 4-5 days (the worst ones), more manageable!</p>
<p>I’ve also learnt that only 1 in 10 people on PCV chemotherapy get the jaw / mouth side effects like I have! I mean, I wouldn’t expect it any other way given the rarity of the type of tumour I have, where’s the fun in that, huh, body? What a stupid bag of meat.</p>
<p>Lastly and probably most important is the types of foods I can and can’t eat. After day 2 normal breakfast is out the window and replaced with food with an actual taste. By Day 5 my mouth starts to feel slightly better, at day 12 I was almost back to normal.</p>
<p>Now of course this is assuming the other 5 cycles are exactly the same as the first, but the Oncology team did say there would likely be a slight cumulative effect as the cycles progress, so prepare myself for it to get worse over time!</p>
<p>All in all, though, given the number of <a href="https://www.macmillan.org.uk/cancer-information-and-support/treatments-and-drugs/pcv">possible side effects</a>, I feel I’ve got off lightly for my first cycle. I’m just hoping it stays that way for the following 5! But that’s totally out of my control. Here’s hoping my white blood count stays high enough to continue, and I don’t catch any illness’ over the next 8 months! This may be a big ask given I’m currently starting a phased return to work!</p>
<h2 id="ongoing-work---months-of-march--april-2022">Ongoing work - Months of March / April 2022</h2>
<p>Now, considering I’ve only been working a limited number of hours during my “phased return to work” period, I’ve felt It’s been very productive. Obviously not 100%, but I set myself a daily set of tasks I’d like to complete, then anything that doesn’t get done gets “bumped” onto the next day or later in the week. Obvious really when you think about it, but I’m really having to be that strict with myself, else I know I’ll end up doing full days again and burning myself out! Not good for me (or work either!). I’ve been logging all my working hours and what I’ve been doing to see what pattern emerged over my “recovery periods”. Interestingly, you can see a pattern. As time passes and the drugs work out of my system, I’m able to work slightly longer and achieve more each day. Some highlights at work for the past months include:</p>
<ul>
<li><strong>Recruitment</strong>: The community and I are very much recruiting FE developers. I’ve been involved in all stages of this (CV sift, phone interview and face-to-face interviews).</li>
<li><strong>Joiners</strong>: We’ve had a number of new joiners in the community. So I’ve been involved in welcoming them to the department and making sure our guidance around the communities new starter “buddy system” is set out and well-documented.</li>
</ul>
<p>But the absolute highlight for me has to be the GOV.UK team has managed to remove <a href="https://jquery.com/">jQuery</a> from all Frontend apps across GOV.UK.</p>
<p>Sorry to go into “work mode”, but this basically means 32 KB of compressed JavaScript has been removed that our users no longer need to download when they visit all GOV.UK pages. Now I know what you may be thinking, 32 KB that’s nothing! So that’s true for a single user, but GOV.UK see’s millions of users every month, so it multiplies to a huge data saving for all our users! I tweeted about the results we saw in our performance monitoring (<a href="https://twitter.com/TheRealNooshu/status/1509487050122276864">RUM</a>, <a href="https://twitter.com/TheRealNooshu/status/1511995957483048960">Synthetic</a>) and the response was quite crazy!</p>
<p>The performance data speaks for itself, really. I highlighted the fact that this has improved the web performance of GOV.UK for the users who really need it (our P95 users), These are users on limited data plans and low-spec devices. By improving performance for these users actually improves performance for everyone!</p>
<p>So a massive pat on the back to the GOV.UK team for getting this change out the door it’s been a slog but if you don’t remove tech-debt it will eventually drown you and stop you from moving forwards.</p>
<p>We also had some sad news in the community, Alex J a fantastic Lead Frontend Developer and great friend and colleague decided to leave the department to return home to Romania. I can’t tell you how genuinely gutted I was when I heard the news he was leaving, his dedication and knowledge on GOV.UK and his leadership in the Frontend community will be surely missed! I wish Alex all the best and hope our paths cross again in the future!</p>
<p>Sorry for that digression into work. I’m also happy that I’ve proved that I’m still able to work sans-Gary (the prick!). So now back to the “fun part”, my cancer treatment (what an odd statement!)</p>
<h2 id="oncology-update---wednesday-6th-april-morning">Oncology Update - Wednesday 6th April (morning)</h2>
<p>As always, a busy start to the next cycle. Oncology review in the morning, chemotherapy treatment in the afternoon.</p>
<p>I was genuinely nervous about this meeting, as 2 weeks before I’d had yet another MRI scan. This was the first scan that would be able to “show anything”. Since it takes 3 months for the brain to “repair” after radiotherapy. In the last MRI scan, nothing could be said for certain, as damaged brain matter looks the same as a brain tumour on an MRI scan (talk about ironic!).</p>
<p>But anyway, the meeting went really well. My bloods looked great, and the scan was described as “stable” or in other words what is left of Gary hadn’t grown. The Oncologist at one point also said that the tumour was very difficult to see! I think that’s the best news I could have had! It certainly makes up for the nights leading up to the meeting where I would wake up thinking about it!</p>
<p>The chemotherapy would be going ahead as planned on the same dosage of <a href="https://www.cancerresearchuk.org/about-cancer/cancer-in-general/treatment/cancer-drugs/drugs/vincristine">Vincristine</a> I’d had before.</p>
<h2 id="chemotherapy-cycle-3-start---wednesday-6th-april-afternoon">Chemotherapy cycle 3 start - Wednesday 6th April (afternoon)</h2>
<p>Nothing really to report here, only we turned up an hour early for my appointment! I think my frazzled brain got the time wrong, thankfully the cancer centre were able to fit me in any way. I had the infusion, but then had to wait awhile for my other medication (it takes time to prepare). I think my punctuality caused that issue, not the nursing team!</p>
<p>But once completed, I set off on my merry way with a body full of vincristine and a bag of 90+ tablets.</p>
<h2 id="chemotherapy-cycle-3-end-and-recovery---saturday-16th-april">Chemotherapy cycle 3 end and recovery - Saturday 16th April</h2>
<p>As I’ve found with the previous cycles, it starts off slowly in the first few days, then ramps up around day 3 or 4. This is while the body adjusts to the concoction of drugs sent in to kill cells (mainly the bad ones, but there is friendly fire with chemotherapy as it isn’t targetted like radiotherapy).</p>
<p>I was able to work a couple of hours on the Thursday, which got me thinking “oh, maybe this cycle won’t be so bad”. Boy was I wrong about that! I think I jinxed it, so I won’t be making that mistake again in the future!</p>
<p>I won’t go into a day-by-day account, but let’s just say the cycle turned out to be worse than cycle 1. I’ve never felt so bad for so many days with no end in sight! The fact that I knew I needed to keep taking tablets every day that were only going to make me feel even worse, is a horrible feeling. It’s a real struggle to make yourself take them at some points in the 10-days! Some wonderful side effects from this cycle (in no particular order) have been:</p>
<ul>
<li>Nausea (no vomiting, thankfully)</li>
<li>Weakness (as in, can’t get out of bed or even move)</li>
<li>Headaches (thank you paracetamol!)</li>
<li>Light-headed and dizziness (even when sitting on the sofa)</li>
<li>Spiked blood pressure</li>
<li>Lack of appetite</li>
<li>Random pains all over my body</li>
<li>Insomnia</li>
</ul>
<p>And to top it all off, I now seem to have developed <a href="https://www.nhs.uk/conditions/tinnitus/">Tinnitus</a> in both ears, this is a known side effect of the vincristine and can be temporary or permanent (only time will tell).</p>
<p>So, yes, if you are reading this and are new to chemotherapy it probably isn’t a fun read, but as they say “no pain, no gain” (FYI: don’t say this to a person going through chemo unless you want a stern look or a strongly-worded letter!).</p>
<p><strong>So what have I learned in cycle 3?</strong></p>
<ol>
<li>Don’t go into any new cycle expecting the same side effects from the previous cycles, you are setting yourself for a fall!</li>
<li>Only take the anti-sickness pills if you are actually feeling sick (see my comments below on this)</li>
<li>Make sure you are open and honest with the team managing your treatment, keep them updated regularly! They are there to help you!</li>
<li>Try to focus on the recovery period, it takes a while for the drugs to leave your system. It can take up to 6 weeks for some people (it’s likely you won’t feel “normal” again until this happens).</li>
</ol>
<p>Just to jump back to point 2, there’s conflicting information here. You are told to take them only when you feel sick, but also that it is easier to cure the “feeling of sickness” than the sickness itself! So yes, get in a time machine and see if you are likely to feel sick or be sick, then choose the correct path!</p>
<p>The reason why this point is so important is that each drug you take has its own unique set of side effects for your body. I found the anti-sickness pills were making me extremely weak and tired, so I stopped taking them midway through the cycle. Only then to find that the last few days of the cycle I’d suffer from terrible Insomnia, because I no longer had the weakness and tiredness from the anti-sickness pills! It’s a minefield! I’ll be mentioning all this at my Oncology review meeting at the start of Cycle 4 (May 18th 2022), maybe they can adjust my drugs in some way?</p>
<p>To end on a more positive note, I’m now over half-way through! 3 cycles down, 3 to go! Roll on October, and at some point back to “normality”.</p>
<p>So as before I likely won’t be updating the post again until after my next cycle ends (end of May), unless something drastic happens!</p>
<div id="postBottom"> </div>
<h2 id="diagnosis-anniversary---thursday-21st-april">Diagnosis Anniversary - Thursday 21st April</h2>
<p>So here’s an anniversary that I never thought I’d be ‘celebrating’ in a million years, but here we are. Celebrating is totally the wrong word, but it is a significant date, as 1-year ago today it felt like the whole ground fell from under me. My life, what was once a path paved and walked day by day, suddenly became a fog of unknown obstacles and fear. So many questions that my whole family and I had no idea about! I mean, why would you read into brain cancer if you didn’t need to?!</p>
<p>It has been both the longest and shortest year of my life, when you live it day-to-day everything drags! Radiotherapy is relentless, a 6-week-long tidal wave that never seems to end. Avoiding everyone during chemotherapy is depressing because you are scared to even catch a common-cold! It’s been a real rollercoaster of emotions!</p>
<p>Some particular points that stand out to me:</p>
<p><strong>Scariest moments</strong></p>
<ul>
<li>Seeing the size of Gary for the first time over the video call with the surgeon!</li>
<li>Closely followed by heading (hah!) into the surgery for my <a href="https://www.hey.nhs.uk/patient-leaflet/undergoing-awake-craniotomy/">awake craniotomy</a>, Not knowing if I was going to wake up parallelised, partially-parallelised, or even wake up at all!</li>
</ul>
<p><strong>Low points</strong></p>
<ul>
<li>Obviously being diagnosed with brain cancer is up there at the top of the list, but also telling my wife, and parents and sister of the diagnosis.</li>
<li>The week between initial diagnosis and actually having more detailed information from the hospital (that was a long week!)</li>
<li>Seeing the look on my mom’s face when we were initially shown the size of Gary.</li>
<li>Having those horrible conversations with my wife about what to do if the “worst should happen”</li>
<li>This list could go on for a while, so I’ll spare you and stop at 4!</li>
</ul>
<p><strong>High points</strong></p>
<ul>
<li>Realising there were lots of options available, and I shouldn’t see this as a “death sentence”.</li>
<li>Seeing the outpouring of support both personally (family & friends), at work, and across the whole web community when I first announced my diagnosis.</li>
<li>Being told they’d been able to remove 95% of the tumour with little to no-damage to any of the good parts of my brain.</li>
<li>Finishing radiotherapy after a lot off back-and-forth to the hospital every week day for 6 weeks!</li>
<li>Receiving the <a href="https://www.lego.com/en-gb/product/millennium-falcon-75192">UCS Lego Millennium Falcon</a> from some very kind friends in the Web Performance community (build in progress at the moment!)</li>
<li>Realising I can still think (and work) in the post-Gary world (“full recovery” may take a while, but I can do it!)</li>
<li>My wife raising over £2,700 for <a href="https://www.thebraintumourcharity.org/">The brain Tumour Charity</a> for her entry into the <a href="https://www.tcslondonmarathon.com/">2022 London Marathon</a>. Thanks to everyone who donated, you rock!</li>
<li>Seeing how we have come together as a family to cope with the scary and ever-changing world of the cancer diagnosis and treatment process.</li>
</ul>
<p>To jump on the last point a more. I can’t thank my close family and friends enough over what has been an incredibly tough year! I certainly couldn’t have made it this far without the daily support of my wife, kids, parents, sister and her husband, and my mother-in-law.</p>
<p>Admittedly, my kids are the absolute perfect age to drive me to distraction, but that’s exactly what is needed. Life would be so boring without their daily arguments and tantrums!</p>
<p>And last (but certainly not least!), a huge thank you to the help, support and dedication of the whole of the NHS workers I’ve interacted with over the past year (so so many individuals!). Without them and modern medicine, I hate to think where I would be now, 1-year into my diagnosis!</p>
<h2 id="treatment-tally-table">Treatment Tally Table</h2>
<p>I thought I’d pull together a tally of all the treatment I’ve had along the way in my quest to evict Gary from my skull, which I will keep updated below.</p>
<p>As I’ve said before, thank you again to the NHS. All this would have cost a fortune in <em>certain</em> countries without universal healthcare (sorry American readers)!</p>
<table>
<thead>
<tr>
<th>Treatment</th>
<th>Number</th>
<th>Finished?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Radiotherapy</td>
<td>33</td>
<td>Yes</td>
</tr>
<tr>
<td>Chemotherapy Cycles</td>
<td>3</td>
<td>No</td>
</tr>
<tr>
<td>MRI Scans</td>
<td>5</td>
<td>No</td>
</tr>
<tr>
<td>CT Scans</td>
<td>3</td>
<td>No</td>
</tr>
<tr>
<td>PET Scans</td>
<td>1</td>
<td>Yes</td>
</tr>
<tr>
<td>Major Surgery</td>
<td>1</td>
<td>Yes?</td>
</tr>
</tbody>
</table>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>21/04/21: Tumor first diagnosed.</li>
<li>12/05/21: Initial post published.</li>
<li>15/05/21: Posted 3 new updates from the week. Next appointment on the 20th May.</li>
<li>19/05/21: Added information about another seizure.</li>
<li>21/05/21: Added information about the pre-operative assessment appointment.</li>
<li>25/05/21: Added update about the weekend and another seizure I’ve had.</li>
<li>29/05/21: Added information about obtaining my scan data and a different type of seizure.</li>
<li>03/06/21: Added update on date for memory test and overall symptoms.</li>
<li>07/06/21: Added steroid and reduced seizures update.</li>
<li>10/06/21: Added update from latest Neurologist meeting.</li>
<li>11/06/21: Update on first ‘core feature’ seizure I’ve had in 2 weeks.</li>
<li>19/06/21: Added an update about the sleepy seizure and an epiphany.</li>
<li>18/07/21: Added an updates about the month of July and “Between Life and Death”.</li>
<li>23/07/21: Update on the latest timings and the surgery info I’ve been given.</li>
<li>26/07/21: Pre-surgery MRI scan update.</li>
<li>01/08/21: Scan results and swelling symptoms update.</li>
<li>06/08/21: Back on the ‘roids and finally a productive day!</li>
<li>12/08/21: The highs and lows of steroids and finally some information on an operation date!</li>
<li>23/08/21: Added info about a surgery date and the fact I had another seizure.</li>
<li>27/08/21: Surgery cancelled, and an update on the very long week.</li>
<li>04/09/21: New surgery date is looking promising update, fingers crossed it will be next week!</li>
<li>08/09/21: Added update on the scans today and my final post for a little while.</li>
<li>21/12/21: Added a huge post-surgey update on the recover, radiotherapy and future plans.</li>
<li>17/01/22: Added an update about my chemo meetings and first cycle. Also added a tally table for all the treatment I have recieved so far.</li>
<li>17/01/22: Added an update to my 1p/19q Codeleted gene result, thanks to <a href="https://twitter.com/milesm999">Miles Mandelson</a> for reminding me about this result.</li>
<li>24/01/22: Added update on Cycle 1 and 2 of chemo and the oncology meetings</li>
<li>19/04/22: Added update on Cycle 3 recovery and return to work.</li>
<li>21/04/22: Added update on the 1 year diagnosis anniversary.</li>
</ul>Matt HobbsWednesday 21st April 2021 was a day like no other. This certainly isn't my usual web performance related blog post!How to use WebPageTest’s Graph Page Data view2021-04-13T12:09:00+00:002021-04-13T12:09:00+00:00https://nooshu.com/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view<p>WebPageTest is a mysterious beast. You can use it almost every day for years and every so often a link catches your eye that you’ve never noticed before. It then leads you to a whole new set of functionality that you didn’t know it had. It’s happened so often I’ve actually <a href="/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#hidden-gems">written a whole section about the ‘hidden gems’</a> in my <a href="/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/">‘How to read a WebPageTest waterfall chart</a> blog post. In this blog post I’m going to focus on one of those ‘hidden gems’: the ‘Plot Full Results’ link that sits under the performance results on the ‘Summary’ tab. Unsure what I’m talking about? See the image below:</p>
<figure class="figure">
<a href="/images/plot-graph-view/plot-results-link.png">
<picture>
<source srcset="/images/plot-graph-view/plot-results-link.avif 1098w" type="image/avif" />
<source srcset="/images/plot-graph-view/plot-results-link.webp 1098w" type="image/webp" />
<img loading="lazy" width="1098" height="388" class="figure__image" src="/images/plot-graph-view/plot-results-link.png" alt="On the summary tab, just under the performance table you will see a link to 'Plot Full Results'." />
</picture>
</a>
</figure>
<p>Clicking on this link will bring you to a page with a number of options and graphs available to you. It may be a little confusing as to what you are seeing at first, but let’s go through it together.</p>
<h2 id="context">Context</h2>
<p>First let’s cover what this page is doing, as there isn’t much on the page to really explain it. This page view exists to give you an overview of all the test runs that WebPageTest ran for the site you are testing. WebPageTest will pick the results from the <strong>median</strong> test run for either the ‘SpeedIndex’ or ‘Load Time’ metrics. By default it will use ‘SpeedIndex’ to choose the median run.</p>
<figure class="figure">
<a href="/images/plot-graph-view/runs-annotated.png">
<picture>
<source srcset="/images/plot-graph-view/runs-annotated.avif 1066w" type="image/avif" />
<source srcset="/images/plot-graph-view/runs-annotated.webp 1066w" type="image/webp" />
<img loading="lazy" width="1066" height="314" class="figure__image" src="/images/plot-graph-view/runs-annotated.png" alt="Annotated view of the summary run data." />
</picture>
</a>
</figure>
<p>In the image above the data being shown in the table is from ‘Run 2’ of 3. The reason for multiple runs is because loading the same page under what seems like the same conditions, often returns wildly different results. This is the non-deterministic nature of the web. Variations in network traffic between the test agent and the website being tested (as well as many other factors) creates these variations in the end result. In order to reach a result that accurately represents the <em>actual</em> performance of a website at the given time, WebPageTest runs multiple test runs. It then picks the test run slap bang in the middle for the chosen median metric as the representative run.</p>
<p>This is the reason why you should run as many test runs as possible when using WebPageTest. On the public instance of WebPageTest you are limited to a maximum of 9 test runs. But if you have a private instance you can run as many test runs as you like. Just remember to always pick an odd number of runs, since you are wanting WebPageTest to return the median run. An even number of tests has no median test run, since dividing the total number of runs (minus 1 for the median) by 2 will return a non-integer number. e.g. (8 runs - 1) / 2 = 3.5. How do you pick the 3.5th test run?</p>
<p>But what about if you <em>are</em> interested in the results from the other runs in the test, not just the one WebPageTest has chosen? Well this is where the <code class="language-plaintext highlighter-rouge">graph_page_data.php</code> page helps. It gives you information about important metrics reported across <em>all</em> test runs that completed.</p>
<h2 id="the-graphs">The graphs</h2>
<p>So I’m going to describe this in reverse compared to the actual page layout, as I think it’s easier to explain once you have an idea of what metrics are being plotted in the graphs. The complete list can be seen below:</p>
<ul>
<li>Web Vitals - First Contentful Paint (FCP)</li>
<li>Web Vitals - Largest Contentful Paint (LCP)</li>
<li>Web Vitals - Cumulative Layout Shift (CLS)</li>
<li>Web Vitals - Total Blocking Time (TBT)</li>
<li>Load Time (onload)</li>
<li>Browser-reported Load Time (Navigation Timing onload)</li>
<li>DOM Content Loaded (Navigation Timing)</li>
<li>SpeedIndex</li>
<li>Time to First Byte (TTFB)</li>
<li>Base Page SSL Time</li>
<li>Time to Start Render</li>
<li>Time to Interactive</li>
<li>Time to Visually Complete</li>
<li>Last Visual Change</li>
<li>Time to Title</li>
<li>Fully Loaded</li>
<li>Estimated RTT to Server</li>
<li>Number of DOM Elements</li>
<li>Connections</li>
<li>Requests (Fully Loaded)</li>
<li>Requests (onload)</li>
<li>Bytes In (onload)</li>
<li>Bytes In (Fully Loaded)</li>
<li>Custom Metrics included below.</li>
</ul>
<p>There are a total of 24 metrics tracked in total (at the time of writing).</p>
<h3 id="basic-graph">Basic graph</h3>
<p>Let’s focus on a basic graph and examine it:</p>
<figure class="figure">
<a href="/images/plot-graph-view/lvc-graph.png">
<picture>
<source srcset="/images/plot-graph-view/lvc-graph.avif 710w" type="image/avif" />
<source srcset="/images/plot-graph-view/lvc-graph.webp 710w" type="image/webp" />
<img loading="lazy" width="710" height="550" class="figure__image" src="/images/plot-graph-view/lvc-graph.png" alt="Graph for the last visual change metric." />
</picture>
</a>
</figure>
<p>The graph is for the last visual change metric. Along the horizontal (x-axis) we see each of the 9 runs from our test. Up the vertical (y-axis) we see the time measured in milliseconds. The value from each test run for this metric is represented by a red dot on the graph. As you can see, there’s quite a variance for this metric over the 9 runs. The lowest being run number 2 at 9,043 ms. The highest, run number 5 at 10,920 ms.</p>
<p>But each graph comes with an associated table that extracts important statistical information about the metric across all runs.</p>
<h3 id="basic-table">Basic table</h3>
<p>Below we see the table associated with the graph seen above. Last visual change metric over 9 runs.</p>
<figure class="figure">
<a href="/images/plot-graph-view/lvc-table.png">
<picture>
<source srcset="/images/plot-graph-view/lvc-table.avif 1033w" type="image/avif" />
<source srcset="/images/plot-graph-view/lvc-table.webp 1033w" type="image/webp" />
<img loading="lazy" width="1033" height="104" class="figure__image" src="/images/plot-graph-view/lvc-table.png" alt="Table associated with the last visual change graph from 9 test runs." />
</picture>
</a>
</figure>
<p>Let’s go over each of the columns one by one:</p>
<p><strong>Mean</strong></p>
<p>This is the arithmetic (population) mean, or also commonly referred to as the average value of all the test runs for a selected metric. For this you add all the metric values together and divide by the total number of runs. It’s worth mentioning that the mean can be easily distorted if you have extreme variations higher or lower than the expected value (i.e. a high standard deviation).</p>
<p><strong>Median</strong></p>
<p>I touched on the median value a little earlier in the post. The median (or p50) is the value of the metric that falls right in the middle of the set of results. Each set is the total number of test run by WebPageTest.</p>
<p><strong>p25</strong></p>
<p>p25 stands for the 25th percentile. In web performance terms this is basically saying: “25% of users will have a score for this metric that is better than this value”. Users in this region are likely receiving a good overall experience.</p>
<p><strong>p75</strong></p>
<p>p75 stands for the 75th percentile. The same sentence as used with p25 works here: “75% of users will have a result for this metric that is better than this value”. By improving the performance of a metric at p75, you will be improving it for all users sitting at, or beyond it in the distribution curve (e.g. the low performing 25% long-tail users).</p>
<p><strong>p75 - p25</strong></p>
<p>In statistics this is known as the interquartile range, or IQR. This is giving us an idea of the difference between the 75th percentile and the 25th percentile. If this value is small we know the values for the metric are all fairly close to the median.</p>
<p>In our table we can see that if the last visual change metric value increases by 807 ms, it slips from the 25th percentile to the 75th percentile. IQR is a good measure of variation because it doesn’t take outliers into account. They are removed from the calculation since it is only focussing on the inner 50% of the data where most results will be clustered.</p>
<p>In terms of web performance we want to minimise this value. We want to pull p75 users up closer to p25 users so everyone receives a better experience.</p>
<p><strong>StdDev</strong></p>
<p>This stands for ‘standard deviation’, which is represented with the Greek letter σ (sigma) . It tells us how spread out our data set is from the mean. The standard deviation can be used to tell us if a result is statistically significant, or part of the expected variation. See the <a href="https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule">“68–95–99.7 rule”</a> for more information on how standard deviation can be used to see if a result is expected, or an outlier.</p>
<p>A low standard deviation means the data is clustered close to the mean value. The data is much more predictable and the effect of variance is lower. We will see fewer outliers in the interquartile range. A high standard deviation tells us the data is dispersed over a wider range from the mean.</p>
<p>A lower standard deviation in a web performance context means the specific metric is more stable and predictable.</p>
<p><strong>CV</strong></p>
<p>CV stands for the Coefficient of Variation. This is the measurement of relative variability. It is simply the standard deviation divided by the mean, which produces a ratio. It is presented in the table as a percentage which is an optional, but sometimes more understandable step.</p>
<p>CV is essentially a normalisation process. It is useful when you want to directly compare sets of results that have different measures. E.g. If test A has a CV of 8%, and test B a CV of 15%, you would say test B has more variation than test A, relative to its mean.</p>
<p>In summary, all of the above values are giving us some insights into the spread and variance of the data from the test runs for a specific metric. For web performance, high variation is bad as it shows instability and less predictability for a metric.</p>
<p><strong>Repeat view row</strong></p>
<p>The ‘Repeat View’ row isn’t seen in the above image but there is an option to run and view the repeat view data. ‘First view’ data comes from a browser under ‘cold cache’ conditions. By that I mean all assets need to be downloaded, nothing is cached.</p>
<p>If you have ‘Repeat View’ selected there’d be a second row below with another set of results for when the browser loads with a ‘warm cache’ scenario. Comparing these two rows of data gives you some insight into the effect caching has on the metric being examined.</p>
<h3 id="configuration-options">Configuration options</h3>
<p>Now that we’ve gone over the basic graph and table for a specific metric, it’s a good time to look at the configuration options available to us at the top of the page:</p>
<figure class="figure">
<a href="/images/plot-graph-view/plot-options.png">
<picture>
<source srcset="/images/plot-graph-view/plot-options.avif 1060w" type="image/avif" />
<source srcset="/images/plot-graph-view/plot-options.webp 1060w" type="image/webp" />
<img loading="lazy" width="1060" height="370" class="figure__image" src="/images/plot-graph-view/plot-options.png" alt="Options available to us at the top of the page." />
</picture>
</a>
</figure>
<p>Let’s examine these one by one.</p>
<p><strong>View (First/Repeat)</strong></p>
<p>Pretty self-explanatory really. Which results do you want to be plotted to the graphs? I’d say most of the time you always want the first view results plotted to see the metrics under ‘cold cache’ conditions. If you have run a repeat view, then you can enable this to see how the same metric changes under ‘warm cache’ conditions.</p>
<p><strong>Median (Of plotted metric)</strong></p>
<p>When selected, WebPageTest will draw a line on the graph through the run that has been selected as the median.</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-median-selected.png">
<picture>
<source srcset="/images/plot-graph-view/graph-median-selected.avif 801w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-median-selected.webp 801w" type="image/webp" />
<img loading="lazy" width="801" height="555" class="figure__image" src="/images/plot-graph-view/graph-median-selected.png" alt="Median run highlighted on the graph in red." />
</picture>
</a>
</figure>
<p>In the graph above for Speed Index we can see the median run that has been selected is from run number 3, and it has a value of approximately 7,750.</p>
<p><strong>Median (Run with median metric SpeedIndex)</strong></p>
<p>By default, the median run for the graphs is selected by examining the Load Time metric. The run that sits in the middle of all the runs is selected as the representative run for this test. But you can use another metric. Instead of looking at Load Time, we can tell WebPageTest to look at the SpeedIndex metric for the representative run.</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-median-both.png">
<picture>
<source srcset="/images/plot-graph-view/graph-median-both.avif 926w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-median-both.webp 926w" type="image/webp" />
<img loading="lazy" width="926" height="551" class="figure__image" src="/images/plot-graph-view/graph-median-both.png" alt="Both median run metrics highlighted on the same graph." />
</picture>
</a>
</figure>
<p>In the graph above we can see the difference that changing the median metric has on each metric. Two lines are now shown, one for when Load Time is used, the other for when SpeedIndex is used:</p>
<ul>
<li>Load Time: run 9 is selected which has a value of 12,558 ms</li>
<li>SpeedIndex: run 3 is selected which has a value of 11,706 ms</li>
</ul>
<p>This functionality is useful as it gives you a quick way to examine what the difference would be to all the metrics if you change the metric used to select the median run.</p>
<h2 id="statistical-comparison-against">Statistical Comparison Against</h2>
<p>So this is where it starts to get really interesting. It isn’t at all clear, but you can pass in multiple test ID’s into this page view, as well as associated human readable labels to make the graph keys easier to understand. This is done using the following URL pattern:</p>
<p><code class="language-plaintext highlighter-rouge">https://www.webpagetest.org/graph_page_data.php?tests=[TEST-ID-1]-l:[TEST-LABEL-1],[TEST-ID-2]-l:[TEST-LABEL-2],[TEST-ID-3]...</code></p>
<p>But don’t worry if that sounds too laborious I have a tool that <a href="/blog/2021/04/13/how-to-use-webpagetests-graph-page-data-view/#webpagetest-compare-tool">I’ll show you later</a> to make it easier.</p>
<p>What this functionality allows you to do is pass in multiple test ID’s and compare statistics against each other. So for example, say you are testing out a performance improvement and you are interested in seeing how this change has affected each metric at a statistical level. It allows you to do this.</p>
<p>In the dropdown you can select which test run you want to statistically compare all the other runs against. In the example below I’m testing loading a page with 0% packet loss against one with 3% packet loss.</p>
<h3 id="basic-graph---multiple-test-ids">Basic graph - Multiple test ID’s</h3>
<p>Now that we’ve added another test ID, the graphs have changed:</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-multiple-ids.png">
<picture>
<source srcset="/images/plot-graph-view/graph-multiple-ids.avif 926w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-multiple-ids.webp 926w" type="image/webp" />
<img loading="lazy" width="926" height="564" class="figure__image" src="/images/plot-graph-view/graph-multiple-ids.png" alt="Two sets of points are now seen on the graph, along with to median lines." />
</picture>
</a>
</figure>
<p>Each graph now has two sets of points in different colours. We can now compare each set of test runs against each other. On the graph for the SpeedIndex metric we can see each median line has also been drawn:</p>
<ul>
<li>Zero % PL median - run 3 selected with a SpeedIndex of approximately 7,800 ms</li>
<li>Three % PL median - run 4 selected with a SpeedIndex of approximately 11,000 ms</li>
</ul>
<p>Now considering these two sets of tests are comparing the same page load under different packet loss conditions, it makes sense that the 3% packet loss has a slower SpeedIndex. But we can find more data to back this up.</p>
<h3 id="advanced-table">Advanced Table</h3>
<p>Once the ‘Statistical Comparison Against’ dropdown has been populated, a new table appears above each metrics graph. This table gives you a set of statistics that compares multiple sets of data for each metric:</p>
<figure class="figure">
<a href="/images/plot-graph-view/advanced-table.png">
<picture>
<source srcset="/images/plot-graph-view/advanced-table.avif 720w" type="image/avif" />
<source srcset="/images/plot-graph-view/advanced-table.webp 720w" type="image/webp" />
<img loading="lazy" width="720" height="144" class="figure__image" src="/images/plot-graph-view/advanced-table.png" alt="The advanced table gives a set of statistics comparing the sets of results for the chosen metric." />
</picture>
</a>
</figure>
<p>You will notice that the bottom row has a set of blank cells. This is because it is the variant we are comparing all others against. First let’s set some context: We are comparing two populations of data, one with no packet loss, one with 3% packet loss. The question is:</p>
<blockquote>
<p>Does this packet loss have an effect on the SpeedIndex metric, and if so, how likely is it that it just happened at random?</p>
</blockquote>
<p>Let’s go through what each of the columns mean and see if there’s enough evidence to answer the question:</p>
<p><strong>Variant</strong></p>
<p>Pretty self explanatory, which set of data are we looking at in this row. By default the label you provide is used, if not then the test ID is used. Due to the length of a WPT test ID, it breaks the whole table UI and makes the table harder to read if you don’t add your own human readable label (-l).</p>
<p><strong>Count</strong></p>
<p>The number of test runs that were conducted during each test.</p>
<p><strong>Mean +/- 95% Conf. Int</strong></p>
<p>This is a confusing cell when you first look at it, but it actually contains 2 numbers. The first is the mean for the metric, the second is the value above and below the mean that covers the 95% <a href="https://en.wikipedia.org/wiki/Confidence_interval">confidence interval (CI)</a>. This is known as a normal based confidence interval. The 95% is saying that: ‘95% of the data for the test will sit between this range’. The true value for the mean if we had a larger sample size (by having more test runs), has a very high probability of sitting within this range. The probability of observing a value outside of this range is less than 0.05 (or 5%).</p>
<p>Translating this directly to the table we see above:</p>
<ul>
<li>0% packet loss has a mean SpeedIndex of 7,583 ms, and the 95% confidence interval ranges from 6,744 ms to 8,422 ms</li>
<li>3% packet loss has a mean SpeedIndex of 11,041 ms, and the 95% confidence interval ranges from 8,894 ms to 13,188 ms</li>
</ul>
<p><strong>Diff of mean from [selected test]</strong></p>
<p>What’s the difference between the means for each set of tests. In the table above we see that ‘Three PL’ has a mean value that is 3,457 ms higher than that of the 0% packet loss test.</p>
<p><strong>p-value (2-tailed)</strong>
P-value stands for percentage value. The p-value is the probability of obtaining test results at least as extreme as the results actually observed. 2-tailed means we consider values on both ends of the distribution curve (extremely high, or extremely low). The lower the p-value, the more meaningful the result because it is less likely to be caused by noise.</p>
<p>In the table above we see a probability of 0.006 (or 0.6%) is extremely low. So the likelihood of this result randomly occurring is very low.</p>
<p><strong>Significant?</strong>
Whether a result is considered significant depends on the p-value we set for significance <em>before</em> we start testing. In the case of WebPageTest, it uses the 95% confidence interval, this p-value is set at 0.05, or 5%. This corresponds to the 5% that sits “outside” our confidence interval. So there’s a 5% chance the results beyond the 95% confidence interval occurred at random.</p>
<p>In the above table the data has a p-value of 0.006, which means the results <em>are</em> significant, as the p-value is below 0.05 we set for significance. This is why the value in the table is ‘TRUE’ rather than ‘FALSE’.</p>
<p>Or to summarise all the above: the fact that the Speed Index metric for the 3% packet loss test has a much higher mean than the 0% packet loss data isn’t a random occurrence. It’s a significant result that we should pay attention too, and we can feel confident that it’s real. It isn’t a result that is likely to occur just from random variance.</p>
<h2 id="webpagetest-compare-tool">WebPageTest Compare Tool</h2>
<p>So I mentioned a tool to simplify URL generation. Well back in April 2020 I created a very basic tool for generating comparison URLs for WebPageTest which I <a href="/blog/2020/04/22/webpagetest-comparison-url-generator/">blogged about here</a>. The tool is available at <a href="https://wpt-compare.app/">https://wpt-compare.app/</a>. Since writing this post you are reading now, I’ve added a new bit of functionality that will generate both the filmstrip URL (<code class="language-plaintext highlighter-rouge">/video/compare.php</code>) and the graph comparison page (<code class="language-plaintext highlighter-rouge">/graph_page_data.php</code>).</p>
<figure class="figure">
<a href="/images/plot-graph-view/wpt-compare-shot.png">
<picture>
<source srcset="/images/plot-graph-view/wpt-compare-shot.avif 837w" type="image/avif" />
<source srcset="/images/plot-graph-view/wpt-compare-shot.webp 837w" type="image/webp" />
<img loading="lazy" width="837" height="333" class="figure__image" src="/images/plot-graph-view/wpt-compare-shot.png" alt="The wpt-compare tool can be used to generate a filmstrip URL and now the test graph URLs." />
</picture>
</a>
</figure>
<p>To use it simply enter the test URLs and then give each test a more readable label. Click ‘Generate URLs’ and follow the links provided.</p>
<h2 id="real-world-example">Real-world example</h2>
<p>So let’s take a look at a real world example and see what the data from the <code class="language-plaintext highlighter-rouge">/graph_page_data.php</code> page shows us. Here’s the test setup:</p>
<ul>
<li>URL: https://www.lenovo.com/gb/en/</li>
<li>Comparing: Gzip vs Brotli compression</li>
<li>Browser: Chrome Desktop</li>
<li>Connection: 3G (1.6 Mbps/768 Kbps 300ms RTT)</li>
<li>Test runs: 201</li>
</ul>
<p>The reason for choosing this site is because it has a high number of assets compressed using either Gzip or Brotli compression. I counted 40 in total. I used the list that <a href="https://twitter.com/paulcalvano">Paul Calvano</a> posted on <a href="https://twitter.com/paulcalvano/status/1252216874810716162">Twitter here</a> to choose an appropriate website. 201 runs is a lot, so this was run on my own private WebPageTest instance (where there’s no limit on the number of runs you can complete). If you try the same, just be prepared to leave it running overnight!</p>
<p>Testing Brotli vs Gzip compression with WebPageTest is very easy. Just add the following script in the ‘Script’ tab when running the gzip test to force gzip compression:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>addHeader accept-encoding: gzip,deflate
navigate %URL%
</code></pre></div></div>
<p>This adds a request header to all requests that overrides the browsers default. In doing so we are now essentially telling the server we <em>only</em> support gzip compression, so it doesn’t serve us brotli compressed files.</p>
<h3 id="update-removing-dns-lookup-noise">Update: Removing DNS lookup noise</h3>
<p>After posting this blog post to Twitter I had a really useful conversation with <a href="https://twitter.com/TimVereecke">Tim Vereecke</a> and <a href="https://twitter.com/AndyDavies">Andy Davies</a>. Tim raised an excellent point that depending on DNS performance between the runs, this could be adding noise to the results. After all, the DNS performance is out of our control and really has no part to play in the results we are trying to examine. So if we could remove it then we’ll have a cleaner set of results. Thankfully this is entirely possible using WebPageTest scripting:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setDnsName www.lenovo.com www.lenovo.com
navigate %URL%
</code></pre></div></div>
<p>So what is this magic that is happening here? Thankfully <a href="https://twitter.com/patmeenan">Patrick Meenan</a> was on hand to explain how this works:</p>
<blockquote>
<p>setDnsName does lookups for the second param then stores them as overrides in the hosts file, effectively caching the DNS lookups locally.</p>
</blockquote>
<p>But we can do better than this. That’s when Andy suggested a more generic (and flexible) version of the script:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setDnsName %host% %host%
navigate %url%
</code></pre></div></div>
<p>This does the same as the first script, only you are no longer hard-coding the URL’s. <code class="language-plaintext highlighter-rouge">navigate %url%</code> will take whatever URL you have added to the test URL input box and use it within the script.</p>
<p>I reran all 402 test runs to see what difference it made. And it really has made quite a difference to the results. So I’ve added a second version of the graphs to the metrics that were affected by this change.</p>
<h3 id="a-stable-metric-ssl-time---with-dns-lookup">A stable metric: SSL Time - with DNS lookup</h3>
<p>First let’s take a look at a graph from a stable metric.</p>
<figure class="figure">
<a href="/images/plot-graph-view/base-ssl.png">
<picture>
<source srcset="/images/plot-graph-view/base-ssl.avif 827w" type="image/avif" />
<source srcset="/images/plot-graph-view/base-ssl.webp 827w" type="image/webp" />
<img loading="lazy" width="827" height="641" class="figure__image" src="/images/plot-graph-view/base-ssl.png" alt="SSL time is a stable metric, and it can be seen in the resulting graph." />
</picture>
</a>
</figure>
<p>First thing to notice about the graph above is how grouped together the results are. Both the green and red median lines are pretty much on top of each other. And this is confirmed with the values for the means and the CI’s. Gzip’s CI is plus or minus 1.14 ms. So over 201 runs, 95% of the data is going to sit between 330.4 ms and 332.7 ms. That’s an incredibly small range. Brotli has slightly more variation, from 331.96 ms and 347.85 ms. With the p-value of 0.042, there’s a 4.2% chance of one of the run values sitting outside the 95% confidence interval. This is a significant result, that we can feel confident is real.</p>
<p>Overall we can see that Brotli vs Gzip compression has no performance impact on SSL time. Which of course when you think about it, it can’t. So I’m glad we now have graph data to back this up!</p>
<h3 id="a-stable-metric-ssl-time---without-dns-lookup">A stable metric: SSL Time - without DNS lookup</h3>
<p>Since this test is examining only the SSL time in isolation, the DNS lookup time has no impact on this metric. The resulting graph was the same as above.</p>
<h3 id="an-unstable-metric-ttfb---with-dns-lookup">An unstable metric: TTFB - with DNS lookup</h3>
<p>Let us now turn our attention to what looks like a much more unstable metric: the Time to First Byte results:</p>
<figure class="figure">
<a href="/images/plot-graph-view/ttfb-graph.png">
<picture>
<source srcset="/images/plot-graph-view/ttfb-graph.avif 824w" type="image/avif" />
<source srcset="/images/plot-graph-view/ttfb-graph.webp 824w" type="image/webp" />
<img loading="lazy" width="824" height="643" class="figure__image" src="/images/plot-graph-view/ttfb-graph.png" alt="TTFB graph is very interesting with a clear distinction between the 2 sets of data." />
</picture>
</a>
</figure>
<p>I’ll be completely upfront about this graph, I’m very surprised by it. From my understanding, I didn’t <em>think</em> compression would have an impact on TTFB at all, since I’m assuming all compressed asset versions were being cached by the CDN. But the graph shows a distinction between the two compression algorithms, with gzip having both a lower mean and median across 201 test runs. The p-value comes out as 0 (which may be an error due to rounding of significant figures, I’m unsure). Either way it is low compared to our 0.05 significance level set by the 95% confidence interval. So yes, this is a significant result that isn’t just from random variation.</p>
<p>I have a theory about why this could be. Brotli compression at levels 10 & 11 is <a href="https://quixdb.github.io/squash-benchmark/#results-table">incredibly resource intensive</a>. So if for some reason this is happening “on-the-fly” each time, rather than being compressed and then cached, this could be causing the difference. It’s worth noting that Lenovo use Akamai’s Resource Optimizer to do this compression, so I’d guess this isn’t the case. But it’s the only explanation I have at the moment. Any other explanations or ideas please do <a href="https://twitter.com/TheRealNooshu">let me know</a>, I’d love to find out what is going on.</p>
<p>Here’s another point about the ‘graph page data’ view now worth mentioning: If it hadn’t of been for the statistical analysis seen here, this would have been missed. Now ultimately it may turn out to be a false positive that means nothing at all, but at least it is now visible and can be investigated further.</p>
<h3 id="an-unstable-metric-ttfb---without-dns-lookup">An unstable metric: TTFB - without DNS lookup</h3>
<p>Let’s examine the difference DNS lookup time has on TTFB. <strong>Note</strong>: the y-axis scales are different so you can’t directly visually compare.</p>
<figure class="figure">
<a href="/images/plot-graph-view/ttfb-graph-v2.png">
<picture>
<source srcset="/images/plot-graph-view/ttfb-graph-v2.avif 824w" type="image/avif" />
<source srcset="/images/plot-graph-view/ttfb-graph-v2.webp 824w" type="image/webp" />
<img loading="lazy" width="829" height="636" class="figure__image" src="/images/plot-graph-view/ttfb-graph-v2.png" alt="TTFB for the tests where DNS lookup time has been removed." />
</picture>
</a>
</figure>
<p>We are looking at a mean value of 214 ms reduction (1,632-1,417) in the mean time for gzip, and a huge 441 ms reduction (1,754-1,313) in the mean TTFB for brotli. This has given us a significant result now saying that TTFB is improved by using brotli in this instance, with a 104 ms improvement in the mean over gzip.</p>
<h3 id="obvious-winners-bytes-in-onload-and-fully-loaded---with-dns-lookup">Obvious winners: Bytes in (onload and fully loaded) - with DNS lookup</h3>
<p>Well as we are examining compression it’s probably a good idea to look at a metric where we’re pretty sure we’ll see improvements. The total number of bytes in:</p>
<figure class="figure">
<a href="/images/plot-graph-view/bytes-onload.png">
<picture>
<source srcset="/images/plot-graph-view/bytes-onload.avif 819w" type="image/avif" />
<source srcset="/images/plot-graph-view/bytes-onload.webp 819w" type="image/webp" />
<img loading="lazy" width="819" height="638" class="figure__image" src="/images/plot-graph-view/bytes-onload.png" alt="Bytes In (onload) graph shows the number of bytes download up to the onload event firing." />
</picture>
</a>
</figure>
<p>In the above graph we see the number of bytes downloaded by the browser up to the onload event firing. As you can see, Brotli compression has saved 60 KB up to this point in the page load. The p-value is low, and the result is real since significant is set to ‘TRUE’.</p>
<p>Moving onto the fully loaded graph:</p>
<figure class="figure">
<a href="/images/plot-graph-view/bytes-fully-loaded.png">
<picture>
<source srcset="/images/plot-graph-view/bytes-fully-loaded.avif 818w" type="image/avif" />
<source srcset="/images/plot-graph-view/bytes-fully-loaded.webp 818w" type="image/webp" />
<img loading="lazy" width="818" height="644" class="figure__image" src="/images/plot-graph-view/bytes-fully-loaded.png" alt="Bytes In (fully loaded) graph shows the number of bytes download up to the page is fully loaded." />
</picture>
</a>
</figure>
<p>Here we see the total number of bytes downloaded by the browser during the page load. The graph shows 2 distinct strips of green and red, clearly showing the two compression methods in action. The stats show that 44 KB was saved with Brotli compression being used. P-value is 0 meaning it is therefore significant, so is a reliable result.</p>
<p>In both graphs above, what surprises me is that these graphs don’t just show a strip of green and red blobs across the graph. Some variance is actually shown. I have a couple of theories as to why this could be:</p>
<ol>
<li>The CDN is under CPU load so a different version of files is served to ease the bottleneck (e.g. a gzip version is served instead of brotli)</li>
<li>Lenovo could have A/B testing in action on the homepage. Each bucket is slightly different meaning different assets are loaded</li>
</ol>
<p>Again, it’s good that this graph view has exposed this peculiarity. If this were happening on my website I’d like to get to the bottom of why it is happening. With this data it’s now possible to investigate further.</p>
<h3 id="obvious-winners-bytes-in-onload-and-fully-loaded---without-dns-lookup">Obvious winners: Bytes in (onload and fully loaded) - without DNS lookup</h3>
<p>Since these graphs refer to the number of bytes downloaded by the browser, there aren’t affected by the DNS lookup time. They are the same as seen in the graphs seen above.</p>
<h3 id="the-unexpected-fcp-and-start-render---with-dns-lookup">The unexpected: FCP and Start Render - with DNS lookup</h3>
<p>And finally let’s look at a set of graphs that (for me) were quite unexpected, those related to First Contentful Paint (FCP) and start render:</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-fcp.png">
<picture>
<source srcset="/images/plot-graph-view/graph-fcp.avif 822w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-fcp.webp 822w" type="image/webp" />
<img loading="lazy" width="822" height="643" class="figure__image" src="/images/plot-graph-view/graph-fcp.png" alt="First Contentful Paint graph shows Brotli to be slower than Gzip." />
</picture>
</a>
</figure>
<p>In the above FCP graph we see that the mean and the median for gzip compression is actually lower than brotli, meaning the gzip page is rendering slightly quicker than brotli. Again the p-value is 0, so the results look to be trustworthy.</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-start-render.png">
<picture>
<source srcset="/images/plot-graph-view/graph-start-render.avif 859w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-start-render.webp 859w" type="image/webp" />
<img loading="lazy" width="859" height="645" class="figure__image" src="/images/plot-graph-view/graph-start-render.png" alt="Start render graph for gzip looks to be quicker than brotli" />
</picture>
</a>
</figure>
<p>The same observation can be seen with the ‘Start Render’ which is actually totally expected given <a href="https://nooshu.github.io/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#start-render---green-line-">how start render is measured</a>. So this just solidifies the FCP observation above. Brotli start render time is 330 ms slower than gzip at the mean. Again the p-value is 0, so the results look to be trustworthy.</p>
<p>So why could this be happening? Well it’s worth remembering that web performance is a problem space with many dimensions. Fewer bytes down the wire doesn’t <em>always</em> mean quicker. In this case it could be that brotli is slower to decompress on the device, so painting pixels to the screen is slower than gzip. Again this is another theory, but the data and analysis seem to suggest there’s a significant result here that shouldn’t be ignored.</p>
<h3 id="the-unexpected-fcp-and-start-render---without-dns-lookup">The unexpected: FCP and Start Render - without DNS lookup</h3>
<p>Let’s re-examine the FCP and start render metrics with DNS lookup time removed. <strong>Note</strong>: the y-axis scales are different so you can’t directly visually compare.</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-fcp-v2.png">
<picture>
<source srcset="/images/plot-graph-view/graph-fcp-v2.avif 817w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-fcp-v2.webp 817w" type="image/webp" />
<img loading="lazy" width="817" height="643" class="figure__image" src="/images/plot-graph-view/graph-fcp-v2.png" alt="First Contentful Paint graph with DNS lookup time removed." />
</picture>
</a>
</figure>
<p>There’s been a huge change in the mean where brotli comes out a huge 632 ms quicker than gzip for FCP at the mean. Again another significant result with an incredibly low p-value of 0.001!</p>
<p>Moving onto start render:</p>
<figure class="figure">
<a href="/images/plot-graph-view/graph-start-render-v2.png">
<picture>
<source srcset="/images/plot-graph-view/graph-start-render-v2.avif 825w" type="image/avif" />
<source srcset="/images/plot-graph-view/graph-start-render-v2.webp 825w" type="image/webp" />
<img loading="lazy" width="825" height="642" class="figure__image" src="/images/plot-graph-view/graph-start-render-v2.png" alt="Start render graph with DNS removed paints a very different paints quite a different picture." />
</picture>
</a>
</figure>
<p>Where we really see the difference is in the mean values. In this analysis brotli is 512 ms quicker for start render than with gzip, compared to 331 ms slower from the first set of results! Again the p-value is 0 meaning this is a significant result. DNS lookup time really does have an impact on performance.</p>
<h2 id="user-interface-updates">User interface updates</h2>
<p>While writing this blog post I raised a couple of issues and including feature requests for this page view:</p>
<ul>
<li><a href="https://github.com/WPO-Foundation/webpagetest/pull/1482">Fix the layout of the Statistical Comparison table.</a></li>
<li><a href="https://github.com/WPO-Foundation/webpagetest/issues/1484">Allow multiple tables of data for graph_page_data.php when > 1 test ID</a></li>
</ul>
<h3 id="compare-metrics-across-multiple-test-ids">Compare metrics across multiple test IDs</h3>
<p><a href="https://twitter.com/tkadlec">Tim Kadlec</a> kindly looked over these requests and added the ability to compare metrics like Mean, Median, StdDev etc across multiple test runs when more than 1 test ID is provided. So the new statistics tables look like below when passing in multiple test ID’s:</p>
<figure class="figure">
<a href="/images/plot-graph-view/new-stats-view.png">
<picture>
<source srcset="/images/plot-graph-view/new-stats-view.avif 1916w" type="image/avif" />
<source srcset="/images/plot-graph-view/new-stats-view.webp 1916w" type="image/webp" />
<img loading="lazy" width="1916" height="910" class="figure__image" src="/images/plot-graph-view/new-stats-view.png" alt="The new layout of the statistics view showing multiple test ID's in the tables." />
</picture>
</a>
</figure>
<p>In the example above we are comparing the FCP times as various percentages of packet loss are introduced to the connection.</p>
<h3 id="force-graphs-y-axis-to-start-at-zero">Force graphs y-axis to start at zero</h3>
<p>In some cases where comparing across test runs the graph would start at a y-axis value that wasn’t 0. This can sometimes give the impression that the results are better than they actually are, because it amplifies the gains or losses seen. By forcing the y-axis back to 0 gives a more representative view of the data and the results. It is now possible to do this in the config options at the top of the page:</p>
<figure class="figure">
<a href="/images/plot-graph-view/force-zero-y-axis.png">
<picture>
<source srcset="/images/plot-graph-view/force-zero-y-axis.avif 878w" type="image/avif" />
<source srcset="/images/plot-graph-view/force-zero-y-axis.webp 878w" type="image/webp" />
<img loading="lazy" width="878" height="962" class="figure__image" src="/images/plot-graph-view/force-zero-y-axis.png" alt="A new config option allows you to force the y-axis to zero on the graphs." />
</picture>
</a>
</figure>
<p>Many thanks to Tim for fixing the layout and adding these features.</p>
<h2 id="summary">Summary</h2>
<p>In this blog post we’ve examined the seemingly invisible ‘Plot Full Results’ link that sits under the summary table on every WebPageTest result summary page. A link that you may have never even clicked on! We’ve examined what all the graphs and tables are and tried our hand at interpreting the results. On examining some real-world data we’ve pulled out a few unusual results that may have been hard to spot without this page view. We’ve also examined the difference that DNS performance can have on your test results. So consider removing this noise for your own testing in the future.</p>
<p>So next time you run a test using WebPageTest, why not click the ‘Plot Full Results’ link and see what it tells you. Thanks for reading.</p>
<p><strong>Note</strong>: It’s been far too many years than I care to admit since I studied statistics, so there’s been a fair amount of ‘on-the-job’ learning happening while writing this post. So if you happen to spot anything that is totally wrong, or just poorly explained please do <a href="https://twitter.com/TheRealNooshu">let me know</a> and I’ll correct it and give you credit in the ‘post changelog’ below.</p>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>13/04/21: Initial post published. Big thanks to <a href="https://twitter.com/tomnatt">Tom Natt</a> for checking and feeding back on parts of this post during the draft stage.</li>
<li>16/04/21: Added new graphs to show the difference DNS performance can have on the results. Thanks to <a href="https://twitter.com/TimVereecke">Tim Vereecke</a> for the tip, and to <a href="https://twitter.com/AndyDavies">Andy Davies</a> for the script optimisation.</li>
<li>22/05/21: Added information about the UI updates from <a href="https://twitter.com/tkadlec">Tim Kadlec</a>.</li>
</ul>Matt HobbsThe 'Graph Page Data' view is a one of those WebPageTest features that is hidden in plain sight. So lets shine a light on it and see what it can do.Setting up Cloudflare Workers for web performance optimisation and testing2021-03-14T12:09:00+00:002021-03-14T12:09:00+00:00https://nooshu.com/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing<p>I recently published a blog post about <a href="/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/">Cloudflare Worker recipes</a>. It occurred to me after publishing that I’d offered no guidance at all to the reader as to how to get started using them. You need to do that before you can start using the recipes I’d listed. So I aim to rectify that in this follow up blog post where I go through the process step by step, including images.</p>
<p>The aim of this blog post is to get you all setup with a Worker, then write some code to allow us to rewrite the CSS rules on the BBC News homepage so that all the fonts render using <a href="https://en.wikipedia.org/wiki/Comic_Sans">‘Comic Sans’</a>. A very silly objective but one that should teach you the basics.</p>
<p>The famous quote from Ian Malcome (Jeff Goldblum) in Jurassic Park feels very relevant right now:</p>
<blockquote>
<p>Your scientists were so preoccupied with whether they could, they didn’t stop to think if they should.</p>
</blockquote>
<p>So let us proceed… If you already have a Cloudflare account you can skip step 1.</p>
<h2 id="step-1---sign-up">Step 1 - Sign up</h2>
<p>This is dead simple, visit <a href="https://dash.cloudflare.com/sign-up/workers">this URL</a> and create yourself an account.</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-signup-login.png">
<picture>
<source srcset="/images/cf-tutorial/cf-signup-login.avif 977w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-signup-login.webp 977w" type="image/webp" />
<img loading="lazy" width="977" height="558" class="figure__image" src="/images/cf-tutorial/cf-signup-login.png" alt="Image of the login page for Cloudflare Workers." />
</picture>
</a>
</figure>
<h3 id="step-1a---choose-a-subdomain">Step 1a - Choose a subdomain</h3>
<p>Your worker will require a unique subdomain. You will then create your own workers under this unique subdomain. For example in the instance below a Worker named “my-worker” would be deployed to: <code class="language-plaintext highlighter-rouge">https://my-worker.nooshu-test-worker.workers.dev</code>:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-signup-subdomain.png">
<picture>
<source srcset="/images/cf-tutorial/cf-signup-subdomain.avif 977w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-signup-subdomain.webp 977w" type="image/webp" />
<img loading="lazy" width="977" height="830" class="figure__image" src="/images/cf-tutorial/cf-signup-subdomain.png" alt="Choosing a subdomain to deploy your workers." />
</picture>
</a>
</figure>
<h3 id="step-1b---choose-a-plan">Step 1b - Choose a plan</h3>
<p>Next up is time to choose your plan. As you are just starting out you could use the free plan for testing. But for only $5 per month you get a whole bunch of benefits. This post isn’t sponsored by Cloudflare, I promise!</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-signup-plan.png">
<picture>
<source srcset="/images/cf-tutorial/cf-signup-plan.avif 962w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-signup-plan.webp 962w" type="image/webp" />
<img loading="lazy" width="962" height="1047" class="figure__image" src="/images/cf-tutorial/cf-signup-plan.png" alt="Choose your Worker plan, either free or bundled for $5 per month." />
</picture>
</a>
</figure>
<h3 id="step-1c---verify-your-email">Step 1c - Verify your email</h3>
<p>Final part of signing up, check your email to verify your new account!</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-signup-verify.png">
<picture>
<source srcset="/images/cf-tutorial/cf-signup-verify.avif 974w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-signup-verify.webp 974w" type="image/webp" />
<img loading="lazy" width="974" height="690" class="figure__image" src="/images/cf-tutorial/cf-signup-verify.png" alt="Verify your email account to complete signup." />
</picture>
</a>
</figure>
<p>I then had to complete a Google reCAPTCHA after clicking the email link to identify all the boats in the image before being redirected to the login page for the final step in verification:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-signup-verify-login.png">
<picture>
<source srcset="/images/cf-tutorial/cf-signup-verify-login.avif 987w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-signup-verify-login.webp 987w" type="image/webp" />
<img loading="lazy" width="987" height="820" class="figure__image" src="/images/cf-tutorial/cf-signup-verify-login.png" alt="Complete the final verification by logging in." />
</picture>
</a>
</figure>
<h2 id="step-2---create-your-worker">Step 2 - Create your Worker</h2>
<p>After login you will be presented with the screen below. Here we are given 2 routes. Either we create a new Worker using by clicking the big blue ‘Create a Worker’ button to the right of the page, or we can use the <code class="language-plaintext highlighter-rouge">wrangler</code> CLI tool used to wrangle your workers. I’ll show both routes below. <strong>Note</strong>: you only need to use one route, so choose your preferred method.</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-create-screen.png">
<picture>
<source srcset="/images/cf-tutorial/cf-create-screen.avif 972w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-create-screen.webp 972w" type="image/webp" />
<img loading="lazy" width="972" height="725" class="figure__image" src="/images/cf-tutorial/cf-create-screen.png" alt="On the screen we get to create our first worker." />
</picture>
</a>
</figure>
<h3 id="step-2a---using-the-user-interface-ui">Step 2a - Using the user interface (UI)</h3>
<p>This is the quickest method: setup a new Worker is via the UI as it does it all for you. Once clicking on the ‘Create a Worker’ you will be presented with this screen. I’ve annotated it to highlight key areas:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-worker-ui-annotated.png">
<picture>
<source srcset="/images/cf-tutorial/cf-worker-ui-annotated.avif 1378w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-worker-ui-annotated.webp 1378w" type="image/webp" />
<img loading="lazy" width="1378" height="912" class="figure__image" src="/images/cf-tutorial/cf-worker-ui-annotated.png" alt="Annotated screen for the developer tools for the Worker." />
</picture>
</a>
</figure>
<p>The screen seen above gives you everything you need to develop your first Worker. The name of the worker has been automatically generated by the system, but it can be renamed as needed. The Worker script goes in the left panel. It comes pre-populated with a JavaScript hello world application. You can send a request through the Worker and preview the response in the right panel. A preview of the actual webpage can be seen in the ‘Preview’ tab. At the bottom right of the page you may notice a section that looks very familiar. It’s a cut-down version of the Chrome DevTools panel. It allows you to view <code class="language-plaintext highlighter-rouge">console</code> messages, network requests through the worker and also a sources panel for any local sources (<code class="language-plaintext highlighter-rouge">worker.js</code> is seen by default). Saving and deploying the above hello world app makes it viewable at <code class="language-plaintext highlighter-rouge">https://summer-bread-11b8.nooshu-test-worker.workers.dev</code> (this link isn’t live).</p>
<p>The above method works well, but it’s a little restrictive. I’m sure ideally you are going to want to use your own code editor on your machine for most Worker development. This is where <code class="language-plaintext highlighter-rouge">wrangler</code> comes in.</p>
<h3 id="step-2b---using-wrangler">Step 2b - Using <code class="language-plaintext highlighter-rouge">wrangler</code></h3>
<p>Wrangler is the <a href="https://github.com/cloudflare/wrangler">open source</a> CLI tool you can use to manage and deploy your Workers from your local machine. It comes with great documentation on how to set it up, but I’ll cover it here too for completeness. First, I’d recommend creating a new directory for your Worker code.</p>
<p><strong>Install</strong></p>
<p>You can use npm to install <code class="language-plaintext highlighter-rouge">wrangler</code> by using the following command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm install -g @cloudflare/wrangler
</code></pre></div></div>
<p><strong>Authenticate with your Cloudflare account</strong></p>
<p>Running the following command will start the authentication process:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wrangler config
</code></pre></div></div>
<p>This will then ask you to enter an API Token, I go through this process below.</p>
<p><strong>Create an API key process</strong></p>
<p>Via the CLI Quick Start you will find a ‘Get your API token’ button, or you can go to it directly via <a href="https://dash.cloudflare.com/profile/api-tokens">this link</a>:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-api-button.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-api-button.avif 732w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-api-button.webp 732w" type="image/webp" />
<img loading="lazy" width="732" height="671" class="figure__image" src="/images/cf-tutorial/cf-config-api-button.png" alt="On the CLI Quick start there is a blue 'Get your API token' button." />
</picture>
</a>
</figure>
<p>This then brings you to a screen to start the creation process. Click the ‘Create Token’ button:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-api-create.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-api-create.avif 1050w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-api-create.webp 1050w" type="image/webp" />
<img loading="lazy" width="1050" height="647" class="figure__image" src="/images/cf-tutorial/cf-config-api-create.png" alt="Here we see the page where we can start the creation of the token." />
</picture>
</a>
</figure>
<p>We then get the option to create a Token from a set of templates. Click the ‘Use template’ button on the ‘Edit Cloudflare Workers’ row:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-api-template.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-api-template.avif 718w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-api-template.webp 718w" type="image/webp" />
<img loading="lazy" width="718" height="665" class="figure__image" src="/images/cf-tutorial/cf-config-api-template.png" alt="We get the option to create a token from a template." />
</picture>
</a>
</figure>
<p>The ‘Edit Cloudflare Workers’ template will show. Most of it can be left with the defaults. Just populate the dropdowns highlighted in red and click the ‘Continue to summary’ button:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-create-token-page.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-create-token-page.avif 934w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-create-token-page.webp 934w" type="image/webp" />
<img loading="lazy" width="934" height="1129" class="figure__image" src="/images/cf-tutorial/cf-config-create-token-page.png" alt="The template page can be left as default, apart from the zone resources and account resources dropdowns which need to be populated." />
</picture>
</a>
</figure>
<p>Last step in the process is to review the summary page and click the ‘Create Token’ button:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-token-summary.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-token-summary.avif 1068w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-token-summary.webp 1068w" type="image/webp" />
<img loading="lazy" width="1068" height="352" class="figure__image" src="/images/cf-tutorial/cf-config-token-summary.png" alt="Review the summary page and click Create Token." />
</picture>
</a>
</figure>
<p>The final page is the one that gives you the actual token you need for authorisation with Cloudflare. Make sure to copy this down as it is only displayed once for security reasons. If you don’t you will need to repeat the whole process again!</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-config-token-created.png">
<picture>
<source srcset="/images/cf-tutorial/cf-config-token-created.avif 1060w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-config-token-created.webp 1060w" type="image/webp" />
<img loading="lazy" width="1060" height="474" class="figure__image" src="/images/cf-tutorial/cf-config-token-created.png" alt="Page with the created token which you will need to copy to clipboard for authorisation." />
</picture>
</a>
</figure>
<p>When prompted in your terminal, you should paste in the API token you’ve just created above. <code class="language-plaintext highlighter-rouge">wrangler</code> will then run through the configuration process. Once complete you are now authenticated to make changes to your worker file locally.</p>
<p><strong>Start a project from an existing template</strong></p>
<p>The quickest way to get up and running is to use a pre built template. You can do this by running the following command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wrangler generate my-project https://github.com/cloudflare/worker-template
</code></pre></div></div>
<p>This will create a <code class="language-plaintext highlighter-rouge">my-project</code> directory with the required worker files you need. <code class="language-plaintext highlighter-rouge">index.js</code> is where your Worker code exists. The <code class="language-plaintext highlighter-rouge">wrangler.toml</code> file is the configuration for this Worker. You should edit this file and add in your <code class="language-plaintext highlighter-rouge">account_id</code>. This can be found on the Workers overview page:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-worker-overview.png">
<picture>
<source srcset="/images/cf-tutorial/cf-worker-overview.avif 1067w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-worker-overview.webp 1067w" type="image/webp" />
<img loading="lazy" width="1067" height="591" class="figure__image" src="/images/cf-tutorial/cf-worker-overview.png" alt="On the worker overview page you will find the account ID you need for the wrangler.toml file." />
</picture>
</a>
</figure>
<p>Once done, save the edited <code class="language-plaintext highlighter-rouge">wrangler.toml</code> file.</p>
<p><strong>Preview the worker</strong></p>
<p>Last step is to preview your new worker. You can do this by running the command below (make sure you are in the Worker directory we created with above):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wrangler preview
</code></pre></div></div>
<p>This will output a URL to the command line and automatically open your default browser with your new Worker visible:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-worker-started.png">
<picture>
<source srcset="/images/cf-tutorial/cf-worker-started.avif 1021w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-worker-started.webp 1021w" type="image/webp" />
<img loading="lazy" width="1021" height="585" class="figure__image" src="/images/cf-tutorial/cf-worker-started.png" alt="Very simple sample worker has now been created" />
</picture>
</a>
</figure>
<p>Now admittedly this isn’t the most exciting example, but we now have a solid base to work from. When it comes to editing and redeploying this code simply edit the <code class="language-plaintext highlighter-rouge">index.js</code> file with your changes then run:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wrangler publish
</code></pre></div></div>
<p>This command will push your changes up to the Worker that you can then view on the Worker subdomain you chose earlier. For this example it was <code class="language-plaintext highlighter-rouge">https://my-project.nooshu-test-worker.workers.dev</code>.</p>
<p>That now completes the setup of the Worker, so we can finally move onto the more interesting part: Making it actually do something useful!</p>
<h2 id="step-3---write-some-code">Step 3 - Write some code</h2>
<p>So my basis for the Worker code is the boilerplate code I listed in my <a href="/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/">‘Cloudflare Worker recipes for frontend performance testing’</a>, which you can <a href="https://gist.github.com/Nooshu/8b018e39ed6b35c4640e88f5eee71d91">find here</a>. We are going to take this code and use it as a template to modify the BBC News homepage.</p>
<p><strong>What do you want to change?</strong></p>
<p>So the next decision for you to make is what exactly do you want to change on the site you are testing? I’ve compiled some common web performance tasks in the <a href="/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/">recipes blog post</a>, but it’s completely up to you as to what you want to change. The question to ask yourself is: “What changes can I make to this page to improve performance?”. Now the experiments themselves may actually make no difference at all, or even make performance worse! But at least you’ve found that out early in the process, allowing you to iterate quickly and try again.</p>
<p>So for this silly example I’m simply going to change the BBC News font from the standard font to:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">font-family</span><span class="o">:</span> <span class="s1">"Comic Sans MS"</span><span class="o">,</span> <span class="s1">"Comic Sans"</span><span class="o">,</span> <span class="nt">cursive</span><span class="o">;</span>
</code></pre></div></div>
<p>It’s a completely pointless example but it illustrates how you can use a Worker to manipulate a page.</p>
<p><strong>Working locally in the browser</strong></p>
<p>So before diving into the Worker code you may wish to experiment locally first to work out exactly what changes you would like to make on the page, and persist them across page reloads. For this I’d recommend the incredibly useful Chrome feature called <a href="https://developers.google.com/web/updates/2018/01/devtools#overrides">‘Local Overrides’</a>. Local overrides allow you to make modifications to a page asset on your local machine, that will then persist across page reloads. So the browser will choose to use your local copy, rather than the network version from the website.</p>
<p><strong>Working in the ‘cloud’</strong></p>
<p>Another option you have is to work within the Cloudflare Worker UI I described in <a href="#step-2a---using-the-user-interface-ui">Step 2a - Using the user interface (UI)</a>. The advantage with this method is you get instant feedback. You can tweak the code on the left hand side and instantly see the result reflected in the response and/or preview window. In the image below you can see that I have:</p>
<ul>
<li>Made a request for the exact CSS file that I want to change <code class="language-plaintext highlighter-rouge">https://m.files.bbci.co.uk/modules/bbc-morph-news-page-styles/2.4.1/enhanced.css</code></li>
<li>Set the <code class="language-plaintext highlighter-rouge">x-host: www.bbc.co.uk</code> header.</li>
<li>Set the <code class="language-plaintext highlighter-rouge">Accept: text/css,*/*;q=0.1</code> header (since a direct request to the resource doesn’t set it automatically).</li>
<li>Modified the body <code class="language-plaintext highlighter-rouge">font-family</code> from <code class="language-plaintext highlighter-rouge">Helvetica,Arial,freesans,sans-serif</code> to <code class="language-plaintext highlighter-rouge">'"Comic Sans MS", "Comic Sans", cursive;'</code>.</li>
</ul>
<figure class="figure">
<a href="/images/cf-tutorial/cf-ui-editing.png">
<picture>
<source srcset="/images/cf-tutorial/cf-ui-editing.avif 2503w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-ui-editing.webp 2503w" type="image/webp" />
<img loading="lazy" width="2503" height="1145" class="figure__image" src="/images/cf-tutorial/cf-ui-editing.png" alt="Cloudflare UI with the example code and modified CSS." />
</picture>
</a>
</figure>
<p>We can see the modified response body in the built in DevTools console window, so we know that the changes we’re expecting are reflected in the resulting response. These changes once deployed to the Worker will modify all CSS files that pass through it., but there’s a recipe where I restrict that to a single CSS file if needed.</p>
<p>One annoying downside to working in the UI is it’s quite restrictive in terms of space for editing. So if you want to use your local code editor the complete code from the above image can be found in <a href="https://gist.github.com/Nooshu/d45a48e8f5aa6ed758889c7953fee286">this gist</a>. If you prefer a version without the comments, <a href="https://gist.github.com/Nooshu/a546aaba23b6f64eb4812d39387c87c1">see here</a>.</p>
<p><strong>Working in your text editor</strong></p>
<p>The last option, and the one I find most convenient is working from my standard code editor, since this fits in with my usual workflow. Once set up, it is dead simple to make changes. Simply edit the <code class="language-plaintext highlighter-rouge">index.js</code> file in the worker directory we created earlier with the changes you want, then publish the changes to Cloudflare using <code class="language-plaintext highlighter-rouge">wrangler publish</code>. These changes will be deployed instantly and also automatically reflected in the Worker UI we used earlier.</p>
<h2 id="step-4---web-performance-testing">Step 4 - Web performance testing</h2>
<p>So what’s the point in making web performance changes if you can’t measure the difference between results? This is where WebPageTest comes into play. Once configured we can easily test our website performance before and after the modifications and compare the results to see if we have improved performance (or made it worse!)</p>
<p>For this we are going to use some basic <a href="https://docs.webpagetest.org/scripting/">WebPageTest scripting</a>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>overrideHost www.bbc.co.uk my-project.nooshu-test-worker.workers.dev
overrideHost m.files.bbci.co.uk my-project.nooshu-test-worker.workers.dev
navigate https://www.bbc.co.uk/news
</code></pre></div></div>
<figure class="figure">
<a href="/images/cf-tutorial/cf-wpt-setup.png">
<picture>
<source srcset="/images/cf-tutorial/cf-wpt-setup.avif 1071w" type="image/avif" />
<source srcset="/images/cf-tutorial/cf-wpt-setup.webp 1071w" type="image/webp" />
<img loading="lazy" width="1071" height="646" class="figure__image" src="/images/cf-tutorial/cf-wpt-setup.png" alt="Basic setup of how to run a scripted test in WebPageTest." />
</picture>
</a>
</figure>
<p>What the code is doing above is redirecting requests the WebPageTest browser is making to specific domains through the Worker so they can be modified as required. WebPageTest will automatically add a different <code class="language-plaintext highlighter-rouge">x-host</code> request header to the requests coming from each domain when it does this.</p>
<p>Simply edit the above script as needed and then run a test. If you are unsure how to run a test, I have a <a href="/blog/2020/12/31/how-to-run-a-webpagetest-test/">blog post here</a> all about it.</p>
<h3 id="note-about-the-x-bypass-transform-header">Note about the <code class="language-plaintext highlighter-rouge">x-bypass-transform</code> header</h3>
<p>So in many of the recipes written with WPT and CFW’s there’s sometimes a custom header (<code class="language-plaintext highlighter-rouge">x-bypass-transform</code>) you can add to bypass any page transformations. Using this allows for a more accurate web performance measurement since you are running the ‘baseline’ page (as in no changes), as well as your modifications through your Worker. This makes them more directly comparable. Unfortunately in the example detailed above CORS reared it’s ugly head and broke all the font loading when <em>any</em> custom headers were added to the test using the <code class="language-plaintext highlighter-rouge">addHeader</code> WebPageTest script. So I stripped out the bypass code from the example gist for this particular test.</p>
<p>What I believe is happening is the custom header is being added to the font request like so: <code class="language-plaintext highlighter-rouge">access-control-request-headers: x-custom-header-123</code>. This is triggering a <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">preflight request</a> (<code class="language-plaintext highlighter-rouge">:method: OPTIONS</code>) to check to see if this header is allowed. The server responds back with <code class="language-plaintext highlighter-rouge">access-control-allow-headers: *</code> and then the font fails with a 403. This makes absolutely no sense to me at all. I assume the preflight is failing authorisation for reasons… then causing the 403. So yes, I still hate CORS! If anyone has an explanation as to why adding <em>any</em> header to a request ends up killing the web fonts responses, please do <a href="https://twitter.com/TheRealNooshu">let me know</a>!</p>
<h2 id="step-5---result">Step 5 - Result</h2>
<p>That’s it, you’re done. If all has gone to plan the results of your modifications should be visible within the WebPageTest filmstrip. The requests and responses have been routed through our Worker, with the response being modified on the fly before being received by the WebPageTest browser agent:</p>
<figure class="figure">
<a href="/images/cf-tutorial/cf-waterfall-routing.png">
<picture>
<source srcset="/images/cf-tutorial/cf-waterfall-routing.webp 1100w" type="image/webp" />
<img loading="lazy" width="1100" height="148" class="figure__image" src="/images/cf-tutorial/cf-waterfall-routing.png" alt="The route BBC domain and the CSS subdomain being routed through our worker." />
</picture>
</a>
</figure>
<p>The results of my silly change can be seen below:</p>
<h3 id="before">Before</h3>
<figure class="figure">
<a href="/images/cf-tutorial/cf-before.jpg">
<picture>
<source srcset="/images/cf-tutorial/cf-before.avif 1024w" type="image/avif" />
<img loading="lazy" width="1024" height="696" class="figure__image" src="/images/cf-tutorial/cf-before.jpg" alt="Image of the BBC News Homepage before my CFW change. STandard fonts are shown." />
</picture>
</a>
</figure>
<h3 id="after">After</h3>
<figure class="figure">
<a href="/images/cf-tutorial/cf-after.jpg">
<picture>
<source srcset="/images/cf-tutorial/cf-after.avif 1024w" type="image/avif" />
<img loading="lazy" width="1024" height="696" class="figure__image" src="/images/cf-tutorial/cf-after.jpg" alt="Image of the BBC News Homepage after my CFW change. The body font has been changed to Comic Sans." />
</picture>
</a>
</figure>
<p>The before image shows the standard page being run through our Worker with no changes made. In the after image I’ve swapped out the font for <code class="language-plaintext highlighter-rouge">'Comic Sans'</code>. It’s a simple visual example that has no performance effect on the page as all the fonts are still loaded anyway, but using the same methods listed above you can <a href="/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing/">make many different changes</a> to a webpage and test the difference it makes to the page performance. And as a bonus no production code was harmed during this experiment!</p>
<h2 id="summary">Summary</h2>
<p>We’ve covered a lot in this post. Setting up a Cloudflare account all the way through to writing Worker code to modify a CSS file on the fly. Hopefully this post will have introduced you to the basics of using Cloudflare Workers for web performance testing. If you have any feedback, or recipes of your own please do <a href="https://twitter.com/TheRealNooshu">let me know</a>! P.S. this worker used in this blog post has now been deleted, so <code class="language-plaintext highlighter-rouge">my-project.nooshu-test-worker.workers.dev</code> will give you an error if you try it.</p>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>14/03/21: Initial post published.</li>
</ul>Matt HobbsA step by step guide on how to get started with using Cloudflare Workers for web performance optimisation.Cloudflare Worker recipes for frontend performance testing2021-03-02T09:09:00+00:002021-03-02T09:09:00+00:00https://nooshu.com/blog/2021/03/02/cloudflare-worker-recipes-for-frontend-performance-testing<p><strong>Table of contents</strong></p>
<ul>
<li><a href="#what-are-cfws">What are CFW’s?</a></li>
<li><a href="#why-only-frontend-web-performance">Why only frontend web performance?</a></li>
<li><a href="#practical-use-case">Practical use case</a></li>
<li><a href="#boilerplate-code">Boilerplate code</a>
<ul>
<li><a href="#code-quality">Code quality</a></li>
</ul>
</li>
<li><a href="#recipes">Recipes</a>
<ul>
<li><a href="#checking-all-requests-flow-through-the-worker">Checking all requests flow through the worker</a></li>
<li><a href="#adding-resource-hints-to-the-page-head">Adding resource hints to the page <code class="language-plaintext highlighter-rouge"><head></code></a></li>
<li><a href="#adding-resource-hints-using-the-link-header">Adding resource hints using the link header</a></li>
<li><a href="#removing-resource-hints-from-the-page-head">Removing resource hints from the page <code class="language-plaintext highlighter-rouge"><head></code></a></li>
<li><a href="#removing-resource-hints-headers">Removing resource hints headers</a></li>
<li><a href="#modifying-css-and-javascript-response-bodies">Modifying CSS and JavaScript response bodies</a></li>
<li><a href="#removing-elements">Removing elements</a></li>
<li><a href="#clearing-and-adding-inline-scripts">Clearing and adding inline scripts</a></li>
<li><a href="#adding-inline-css">Adding inline CSS</a></li>
<li><a href="#adding-attributes">Adding attributes</a></li>
<li><a href="#quickly-adding-scripts-to-the-head-and-closing-body-tags">Quickly adding scripts to the <code class="language-plaintext highlighter-rouge"><head></code> and closing <code class="language-plaintext highlighter-rouge"><body></code> tags</a></li>
</ul>
</li>
<li><a href="#testing-performance">Testing performance</a>
<ul>
<li><a href="#in-the-browser">In the browser</a></li>
<li><a href="#using-webpagetest">Using WebPageTest</a></li>
</ul>
</li>
<li><a href="#summary">Summary</a></li>
<li><a href="#further-reading">Further Reading</a></li>
</ul>
<p><a href="https://workers.cloudflare.com/">Cloudflare Workers (CFW)</a> (or Workers) have very much become part of my workflow when it comes to examining and ultimately improving frontend web performance (hopefully!). I use them both for work and personal projects. This post I’ve mainly written for myself, as I’ve found I’ve been repeating certain recipes over and over again. So it feels like a good idea to have a place to log them for reference in the future. I’d love to get feedback on what could be improved, or any other examples you may have. If you do, please do <a href="https://twitter.com/TheRealNooshu">Let me know</a>!</p>
<p><strong>Note</strong>: If you are unsure where to start with these recipes I’ve written a <a href="/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/">step by step guide on how to get started with Cloudflare Workers here</a>.</p>
<h2 id="what-are-cfws">What are CFW’s?</h2>
<p>Workers are a serverless offering from Cloudflare built upon their Content Delivery Network (CDN). They provide a lightweight JavaScript execution environment built on the <a href="https://v8.dev/">v8 JavaScript engine</a>. There are many uses for Workers. Many examples are listed <a href="https://developers.cloudflare.com/workers/tutorials">on the Worker documentation page</a>, including <a href="https://developers.cloudflare.com/workers/tutorials/build-a-slackbot">How to build a Slackbot</a>, and <a href="https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator">How to build a QR code generator</a>. But I’m going to focus on a particular area that I find them useful for: frontend web performance.</p>
<h2 id="why-only-frontend-web-performance">Why only frontend web performance?</h2>
<p>Back in February 2012, <a href="https://twitter.com/souders">Steve Souders</a> published his now pretty famous blog post <a href="https://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/">‘the Performance Golden Rule’</a>. To quote from the post:</p>
<blockquote>
<p>80-90% of the end-user response time is spent on the frontend. [So] Start there.</p>
</blockquote>
<p>I highly recommend reading the post, but a quick TL;DR; is essentially: once the server has received a request, done all the database lookups, generated the HTML, and delivered the response to the browser, the rest of the page performance now depends on how it is handled on the frontend. So if you want to make large web performance gains, then focusing on the frontend is a way to do it. I’m sure there are backend optimisations you can explore with Workers, but I won’t be focussing on them in this blog post (although I’d love to hear all about them).</p>
<h2 id="practical-use-case">Practical use case</h2>
<p>To give you a practical example of where Workers come in useful I’ll look at some of the work I do at <a href="https://www.gov.uk/government/organisations/government-digital-service">Government Digital Service (GDS)</a>. The department has built and maintains <a href="https://www.gov.uk/">GOV.UK</a>. It is the website for the UK government. It’s the best place to find policy, announcements, information about the government, and guidance for citizens. Since 2012 it has replaced 1,884 government websites with just one, to become the home of all central government’s online content and services.</p>
<p>What I’m trying to tell you is that it’s a big and complicated bit of software, both on the backend and frontend. The frontend that users see is made up of approximately 13 separate applications, and that’s not including any of the publishing applications. So what would be seen as a “small change” for a small static site, can actually turn out to be a huge change for a large complex site of its size. This is an issue pretty much every organisation and developer will run up against at some point. I’m sure many readers have been in the position where they’ve said: “If <em>only</em> we could change this small piece of code, and examine the difference it makes”, knowing full well that the change is potentially day’s worth of work (if not more).</p>
<p>What we need is a way to experiment with changing production code, without <em>actually</em> making any changes to production code. It doesn’t sound possible does it? But that’s exactly what we can do with Workers. The Worker can sit between the production server and our browser and make changes to server responses on the fly. We can then observe the difference these response changes make to performance. Here’s a tweet about <a href="https://twitter.com/TheRealNooshu/status/1364005363381637122">an example I tried with GOV.UK</a>, examining what difference the position of the JavaScript in the HTML source and the loading mechanism used (e.g. <code class="language-plaintext highlighter-rouge">async</code> / <code class="language-plaintext highlighter-rouge">defer</code>) has on performance. As I mention in the tweet thread, this set of changes would have taken much longer than the hour it took to create the experiments!</p>
<h2 id="boilerplate-code">Boilerplate code</h2>
<p>This code is all based off what <a href="https://twitter.com/andydavies">Andy Davies</a> has written in his excellent blog post all about the subject: <a href="https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/">‘Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers’</a>, which itself was based off the work <a href="https://twitter.com/patmeenan">Patrick Meenan</a> mentions in his <a href="https://www.slideshare.net/patrickmeenan/getting-the-most-out-of-webpagetest">‘Getting the most out of WebPageTest’</a> talk (slide 60). So with this post I really am kneeling on the shoulders of giants!</p>
<p>The heavily commented code that all these recipes are based off can be <a href="https://gist.github.com/Nooshu/8b018e39ed6b35c4640e88f5eee71d91">found in this gist</a>.</p>
<h3 id="code-quality">Code quality</h3>
<p>At this point it’s probably worth mentioning that the code used in the examples is pretty rough and ready “throwaway-code”. It’s simply there to transform certain server responses, rather than being production ready code. That being said, you should always try to <a href="https://dash.cloudflare.com/login">login to the Cloudflare admin area</a> and observe the CPU time that your worker is taking during execution. The free worker plan allows for up to 10ms CPU time per request. If you are going over these 10ms I’d imagine throttling will kick in. This is also a sure sign there are potential inefficiencies in the code which may lead to unrealistic final results (due to increased latency).</p>
<p>For example, I was recently looking to see what a really large HTML page can have on the CPU time in a Worker. I picked the <a href="https://apod.nasa.gov/apod/archivepixFull.html">NASA Astronomy Picture of the Day full Archive</a> which returns 710 KB of uncompressed HTML. For certain DOM manipulation operations I managed to get it to spike to 35ms. And really it’s not a complicated DOM at all, it’s just large:</p>
<figure class="figure">
<a href="/images/cf-worker-performance/worker-graph-spike.png">
<picture>
<source srcset="/images/cf-worker-performance/worker-graph-spike.avif 1037w" type="image/avif" />
<source srcset="/images/cf-worker-performance/worker-graph-spike.webp 1037w" type="image/webp" />
<img loading="lazy" width="1037" height="558" class="figure__image" src="/images/cf-worker-performance/worker-graph-spike.png" alt="In the graph we see the spike in CPU time hitting 35ms for a single request." />
</picture>
</a>
</figure>
<p>So it’s worth checking the CPU graph if you notice latency issues during testing.</p>
<h2 id="recipes">Recipes</h2>
<p>Below you will find a set of recipes I’ve found useful when performance testing.</p>
<h3 id="checking-all-requests-flow-through-the-worker">Checking all requests flow through the worker</h3>
<p>The first thing that’s worth checking is that all the expected responses for your test site are actually flowing through your worker. There are cases when this may not be the case. For example, if some of your requests use relative URLs, and others use absolute URLs. Your absolute URLs will likely be pointing to the original site. This could lead to less accurate results since your browser will be creating a TCP connection to both the worker URL and the original server. Thankfully this pretty simple to fix using the <a href="https://developers.cloudflare.com/workers/runtime-apis/html-rewriter">HTMLRewriter API</a> and the <a href="https://developers.cloudflare.com/workers/examples/rewrite-links">‘Rewrite links’</a> example.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// domain rewriter vars</span>
<span class="kd">const</span> <span class="nx">OLD_URL</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://www.example.com/</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">NEW_URL</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">;</span>
<span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular HTML response for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// rewrite the links from the following page elements</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">a</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">href</span><span class="dl">"</span><span class="p">))</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">href</span><span class="dl">"</span><span class="p">))</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">img</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">src</span><span class="dl">"</span><span class="p">))</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">script</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">src</span><span class="dl">"</span><span class="p">))</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">image</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">src</span><span class="dl">"</span><span class="p">))</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">meta</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">AttributeRewriter</span><span class="p">(</span><span class="dl">"</span><span class="s2">content</span><span class="dl">"</span><span class="p">))</span>
<span class="c1">// transform the old response</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="c1">// https://developers.cloudflare.com/workers/examples/rewrite-links</span>
<span class="kd">class</span> <span class="nx">AttributeRewriter</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">attributeName</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">attributeName</span> <span class="o">=</span> <span class="nx">attributeName</span>
<span class="p">}</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">attribute</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">attributeName</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">attribute</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span>
<span class="k">this</span><span class="p">.</span><span class="nx">attributeName</span><span class="p">,</span>
<span class="nx">attribute</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">OLD_URL</span><span class="p">,</span> <span class="nx">NEW_URL</span><span class="p">),</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In the above example we are picking out selected elements in the page and making sure they all use relative URLs. Note: you should only do this for assets that actually exist on your sites server defined in the <code class="language-plaintext highlighter-rouge">site</code> variable, else you’re just going to get a lot of 404’s!</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/3de82f5aa8cb38e367e6ff5cf730d356">seen in this gist</a>.</p>
<h3 id="adding-resource-hints-to-the-page-head">Adding resource hints to the page <code class="language-plaintext highlighter-rouge"><head></code></h3>
<p>In this example we’re going to look at how we can modify our page <code class="language-plaintext highlighter-rouge"><head></code> to add a few <a href="https://www.w3.org/TR/resource-hints/">resource hints</a>, and give the browser information about the page before it is fully parsed. There are <a href="https://andydavies.me/blog/2019/03/22/improving-perceived-performance-with-a-link-rel-equals-preconnect-http-header/">many articles available</a> to read about how these tags can help improve the performance of a website.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">resourceHints</span> <span class="o">=</span> <span class="s2">`
<link rel="preload" href="/assets/font/font1.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">
<link href="https://cdn.domain.com" rel="preconnect" crossorigin>
`</span>
<span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular HTML response for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// add listed resource hints to the page head</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">head</span><span class="dl">'</span><span class="p">,</span> <span class="k">new</span> <span class="nx">addResourceHints</span><span class="p">())</span>
<span class="c1">// transform the old response</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">addResourceHints</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// notice how we are prepending the hints, right after the opening head tag</span>
<span class="c1">// can be changed to append if you want them right before the closing tag</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">prepend</span><span class="p">(</span><span class="nx">resourceHints</span><span class="p">,</span> <span class="p">{</span><span class="na">html</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice how we have different resource hints being added. In the example I’ve added a <code class="language-plaintext highlighter-rouge">preload</code>, <code class="language-plaintext highlighter-rouge">dns-prefetch</code>, and a <code class="language-plaintext highlighter-rouge">preconnect</code>. You can add as many or as few as you need. Also note how these tags are being added directly after the opening <code class="language-plaintext highlighter-rouge"><head></code> tag. You can change this too if you so wish (e.g. using <code class="language-plaintext highlighter-rouge">.append()</code>).</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/aa0c994fdcaf4b4024798e4253a6bb0d">seen in this gist</a>.</p>
<h3 id="adding-resource-hints-using-the-link-header">Adding resource hints using the link header</h3>
<p>So we’ve added resource hints by modifying the HTML in the <code class="language-plaintext highlighter-rouge"><head></code>, but it is also possible to <a href="https://www.w3.org/TR/resource-hints/#fetching-the-resource-hint-link">use a response header</a> instead. This is quite simple to do with a Worker, but there are a couple of caveats:</p>
<ul>
<li>Be careful of absolute / relative URLs with your preloaded assets. Make sure the URL in the <code class="language-plaintext highlighter-rouge">preload</code> matches how the asset would usually load in the page. If they don’t match you will end up preloading <em>and</em> loading the asset twice (e.g. a font is preloaded and then also loaded through the usual page load).</li>
<li>If using <code class="language-plaintext highlighter-rouge">preload</code> make sure to include the ‘nopush’ string. Cloudflare converts a link preload to a <a href="https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/">HTTP/2 server push</a> by default. This may (or may not) be the functionality you actually want.</li>
</ul>
<p>In the below example I’ve also allowed for both HTML changes as well as modification of the response headers.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="cm">/**
* Insert any HTML modifications here
*/</span>
<span class="c1">// apply above modifications to the response</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// Make the headers mutable by re-constructing the already modified response.</span>
<span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">newResponse</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="nx">newResponse</span><span class="p">)</span>
<span class="c1">// add our Link headers</span>
<span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">Link</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1"></assets/fonts/font-file-1.woff2>;rel="preload";as="font";crossorigin; nopush, </assets/js/js-file-1.js>;rel="preload";as="script"; nopush</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">response</span>
<span class="p">}</span>
<span class="c1">//...</span>
</code></pre></div></div>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/18de65027b3249e407dc97b3aaf7fe61">seen in this gist</a>.</p>
<h3 id="removing-resource-hints-from-the-page-head">Removing resource hints from the page <code class="language-plaintext highlighter-rouge"><head></code></h3>
<p>I’ve given you an example of how to add resource hints to the <code class="language-plaintext highlighter-rouge"><head></code> so I may as well give you an example of how to remove them too. Remember you can be very specific as to which ones you remove by making use of <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors">Attribute selectors</a>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// blanket example of removing all resource hints</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='preload']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='prefetch']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='dns-prefetch']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='prerender']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='preconnect']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// example were we only remove a selected preload hint for a font</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[rel='preload'][href*='our-woff2-font.woff2']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">removeElement</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/6bf735bc9ab3d065658725b78d04fca8">seen in this gist</a>.</p>
<h3 id="removing-resource-hints-headers">Removing resource hints headers</h3>
<p>So above we added the <code class="language-plaintext highlighter-rouge">Link</code> header for our resource hints, but what about if our server is already serving them and you want to test what difference <em>removing</em> them makes? Well it’s pretty much identical to the above code only we use the <code class="language-plaintext highlighter-rouge">delete()</code> method instead of <code class="language-plaintext highlighter-rouge">set()</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// remove all external scripts from the page</span>
<span class="cm">/**
* Make your HTML changes here
*/</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// Make the headers mutable by re-constructing the already modified response.</span>
<span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">newResponse</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="nx">newResponse</span><span class="p">)</span>
<span class="c1">// delete our Link header(s)</span>
<span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="dl">"</span><span class="s2">Link</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">response</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/52fdb9c91607a5920c5c093668fe300e">seen in this gist</a>.</p>
<h3 id="modifying-css-and-javascript-response-bodies">Modifying CSS and JavaScript response bodies</h3>
<p>The great thing about CFWs is you can easily make changes to responses for other types of responses too. For example, in your Worker you could look for CSS and JavaScript files and make changes to their response bodies. You could quite easily remove whole sections of JavaScript code, or add in brand new selectors to the CSS and observe the results in the page. In the simple example below we are:</p>
<ul>
<li>replacing a common CSS string (the <code class="language-plaintext highlighter-rouge">font-family</code> property value)</li>
<li>adding a simple <code class="language-plaintext highlighter-rouge">console.log()</code> to the end of a JavaScript file</li>
</ul>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="cm">/**
* Make your HTML changes here
*/</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/css</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span><span class="c1">// Change CSS here</span>
<span class="c1">// grab the CSS response</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">);</span>
<span class="c1">// extract the body of the request</span>
<span class="kd">let</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
<span class="c1">// modify the CSS response body</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">body</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/Arial, Helvetica Neue, Helvetica, sans-serif;/gi</span><span class="p">,</span><span class="dl">'</span><span class="s1">Georgia, Times, Times New Roman, serif;</span><span class="dl">'</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/Arial,Helvetica Neue,Helvetica,sans-serif;/gi</span><span class="p">,</span><span class="dl">'</span><span class="s1">Georgia, Times, Times New Roman, serif;</span><span class="dl">'</span><span class="p">)</span>
<span class="c1">// return the modified response</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">headers</span>
<span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">*/*</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span><span class="c1">// Change JavaScript here (uses the generic Accept directive)</span>
<span class="c1">// being granular we only modify a single response for a specific JavaScript file</span>
<span class="k">if</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">().</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">our-specific-js-filename.js</span><span class="dl">'</span><span class="p">)){</span>
<span class="c1">// grab the JS response</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">);</span>
<span class="c1">// extract the JS body of the request</span>
<span class="kd">var</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
<span class="c1">// using template literals we add a console.log to the end</span>
<span class="nx">body</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">body</span><span class="p">}</span><span class="s2"> console.log('String added last');`</span><span class="p">;</span>
<span class="c1">// return the modified response</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span>
<span class="na">headers</span><span class="p">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">headers</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">//...</span>
</code></pre></div></div>
<p>There’s a caveat to the above code. The <code class="language-plaintext highlighter-rouge">Accept</code> header for scripts in <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values#values_for_scripts">all modern browsers</a> is <code class="language-plaintext highlighter-rouge">*/*</code>, so I’ve specifically targeted a single JavaScript file. You can easily do the same with the CSS using the same method.</p>
<p>Also, when you get to the point of <code class="language-plaintext highlighter-rouge">var body = await response.text();</code>, what you do next basically comes down to a JavaScript string manipulation exercise. You could easily add / remove / insert code into the files using the basic <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#instance_methods">string manipulation methods</a>.</p>
<p>Be sure to check to see if your <code class="language-plaintext highlighter-rouge"><script></code> or <code class="language-plaintext highlighter-rouge"><link></code> tags don’t have an <code class="language-plaintext highlighter-rouge">integrity</code> attribute. If they do you have <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity">Subresource Integrity</a> enabled on your JavaScript and CSS assets. If you modify these files on the fly like in the example above, the hash of the file will have changed and the browser will refuse to load it. If that’s the case I’d remove the <code class="language-plaintext highlighter-rouge">integrity</code> attribute using the HTMLRewriter at the same time.</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/b7110cdae45b4dfe4aaac3e2ab48e08e">seen in this gist</a>.</p>
<h3 id="removing-elements">Removing elements</h3>
<p>This is an incredibly simple example, as the HTMLRewriter API does all the heavy lifting for us.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// remove a specific script</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">script[src*='name-of-our-script.js']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// remove the third meta tag in the head</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">head > meta:nth-of-type(3)</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// remove all div elements that start with 'prefix'</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">div[class^='prefix']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// remove all link elements that start with '/assets/' and end with '.css'</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">link[href^='/assets/'][href$='.css']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">removeElement</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You can be really creative with your <a href="https://developers.cloudflare.com/workers/runtime-apis/html-rewriter#selectors">CSS selectors</a> if you need to be, after all the Worker is powered by Chrome’s v8 engine, so it understands all the modern CSS selectors.</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/f4e5890419b807555f2590a2676864c8">seen in this gist</a>.</p>
<h3 id="clearing-and-adding-inline-scripts">Clearing and adding inline scripts</h3>
<p>The problem with inline scripts is that most of the time they are just a simple <code class="language-plaintext highlighter-rouge"><script></code> tag. There are no attributes to use to select them, and their position in the page may not always be consistent in the DOM. So a method I’ve found useful is to remove all inline scripts from a page then rebuild and add them back in where you need them. This gives you fine grain control over the HTML markup coming from the Worker:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// remove inline scripts from the page</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">body > script:not([src])</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// reinsert some inline JavaScript back into the page</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">reinsertInlineScript</span><span class="p">())</span>
<span class="c1">// transform the page</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">removeElement</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">reinsertInlineScript</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">){</span>
<span class="kd">let</span> <span class="nx">inlineScript</span> <span class="o">=</span> <span class="s2">`document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');`</span><span class="p">;</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">prepend</span><span class="p">(</span><span class="s2">`<script></span><span class="p">${</span><span class="nx">inlineScript</span><span class="p">}</span><span class="s2"></script>`</span><span class="p">,</span> <span class="p">{</span><span class="na">html</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Using this method you could completely rebuild where inline scripts sit in the page source, and experiment with what they contain (since they are blocking after all).</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/b9d6a0c14946197d38dacf238a3b1bf0">seen in this gist</a>.</p>
<h3 id="adding-inline-css">Adding inline CSS</h3>
<p>Using pretty much the same method as above you can add inline CSS into the <code class="language-plaintext highlighter-rouge"><head></code> of your page. In doing so we are adding bytes to the browsers critical path, but it can be useful for experimentation. One particular use case could be you are wanted to explore is what difference adding <a href="https://www.smashingmagazine.com/2015/08/understanding-critical-css/">critical CSS</a> to a page does for rendering performance:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// add the inline CSS to the head</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">head</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">addNewCSS</span><span class="p">())</span>
<span class="c1">// transform the page</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">addNewCSS</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">newInlineCSS</span> <span class="o">=</span> <span class="s2">`
body {
border: 10px solid red;
}
`</span><span class="p">;</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s2">`<style></span><span class="p">${</span><span class="nx">newInlineCSS</span><span class="p">}</span><span class="s2"></style>`</span><span class="p">,</span> <span class="p">{</span><span class="na">html</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/af80e8974f73903b1bbb62aed057ab2a">seen in this gist</a>.</p>
<h3 id="adding-attributes">Adding attributes</h3>
<p>There may be times where you want to add certain attributes to elements. A great use case for this in web performance is adding <code class="language-plaintext highlighter-rouge">defer</code> and <code class="language-plaintext highlighter-rouge">async</code> attributes to <code class="language-plaintext highlighter-rouge"><script></code> tags, and seeing how this changes the loading performance of a page. In the example below we create classes to add these attributes to scripts. These can be adapted to add any attributes you might want to add to an element:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// add defer to this script</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">script[src*='script-to-be-deferred.js']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">addDeferAttribute</span><span class="p">())</span>
<span class="c1">// add async to this script</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">script[src*='script-to-be-asynced.js']</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">addAsyncAttribute</span><span class="p">())</span>
<span class="c1">// transform the page</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">addDeferAttribute</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">defer</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">defer</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">addAsyncAttribute</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">async</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">async</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/7f66cbfaa832e765e2321212819132f9">seen in this gist</a>.</p>
<h3 id="quickly-adding-scripts-to-the-head-and-closing-body-tags">Quickly adding scripts to the <code class="language-plaintext highlighter-rouge"><head></code> and closing <code class="language-plaintext highlighter-rouge"><body></code> tags</h3>
<p>The position of scripts in the page source has become less of an issue since modern browsers have started to support the <code class="language-plaintext highlighter-rouge">async</code> and <code class="language-plaintext highlighter-rouge">defer</code> attributes, but there still may be cases where you want to position them very specifically in the source for experimentation. This is the method I used when I was investigating reducing the Cumulative Layout Shift (CLS) on GOV.UK <a href="https://twitter.com/TheRealNooshu/status/1364005363381637122">in this tweet</a>. I cleared all scripts from the page source and rebuilt it with the scripts repositioned as required:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//...</span>
<span class="k">if</span><span class="p">(</span><span class="nx">acceptHeader</span> <span class="o">&&</span> <span class="nx">acceptHeader</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">0</span><span class="p">){</span>
<span class="c1">// store this particular request for modification</span>
<span class="kd">let</span> <span class="nx">oldResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="nx">request</span><span class="p">)</span>
<span class="c1">// create a new response</span>
<span class="kd">let</span> <span class="nx">newResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">HTMLRewriter</span><span class="p">()</span>
<span class="c1">// remove all external scripts from the page</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">body > script[src]</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">removeElement</span><span class="p">())</span>
<span class="c1">// insert scripts back before the closing body tag</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">body</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">reinsertBodyScripts</span><span class="p">())</span>
<span class="c1">// insert scripts back before the closing head tag</span>
<span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">"</span><span class="s2">head</span><span class="dl">"</span><span class="p">,</span> <span class="k">new</span> <span class="nx">reinsertHeadScripts</span><span class="p">())</span>
<span class="p">.</span><span class="nx">transform</span><span class="p">(</span><span class="nx">oldResponse</span><span class="p">)</span>
<span class="c1">// return the modified page along with custom headers</span>
<span class="k">return</span> <span class="nx">newResponse</span>
<span class="p">}</span>
<span class="c1">//...</span>
<span class="kd">class</span> <span class="nx">reinsertBodyScripts</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">srcArray</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">/assets/js/body-script-1.js</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">/assets/js/body-script-2.js</span><span class="dl">'</span>
<span class="p">]</span>
<span class="nx">srcArray</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">val</span><span class="p">){</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s2">`<script src="</span><span class="p">${</span><span class="nx">val</span><span class="p">}</span><span class="s2">"></script>`</span><span class="p">,</span> <span class="p">{</span><span class="na">html</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">class</span> <span class="nx">reinsertHeadScripts</span> <span class="p">{</span>
<span class="nx">element</span><span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">srcArray</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">/assets/js/deferred-head-script-1.js</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">/assets/js/deferred-head-script-2.js</span><span class="dl">'</span>
<span class="p">]</span>
<span class="nx">srcArray</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">val</span><span class="p">){</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="s2">`<script src="</span><span class="p">${</span><span class="nx">val</span><span class="p">}</span><span class="s2">" defer></script>`</span><span class="p">,</span> <span class="p">{</span><span class="na">html</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>There are many combinations you may want to experiment with related to <code class="language-plaintext highlighter-rouge">async</code>, <code class="language-plaintext highlighter-rouge">defer</code> and standard blocking scripts. I can’t cover them all here. But hopefully this example gives you an idea of how you’d do it.</p>
<p><strong>Code</strong>: Complete code for this example can be <a href="https://gist.github.com/Nooshu/2de919f6eb8b69c13a3741700bf8742c">seen in this gist</a>.</p>
<h2 id="testing-performance">Testing performance</h2>
<p>So, once you’ve made changes to your page you’re going to want to check what difference they have made to the performance of the page. There are a couple of ways to do this:</p>
<h3 id="in-the-browser">In the browser</h3>
<p>The browser is a great way to check to see if the page is being modified in the way you expect (e.g. view the page source), so I’d recommend setting up this method anyway. Using the <a href="https://bewisse.com/modheader/">ModHeader</a> extension you can easily add request headers to your requests. In our examples above we’d set <code class="language-plaintext highlighter-rouge">x-host</code> to the site we want to change (e.g. <code class="language-plaintext highlighter-rouge">x-host: www.gov.uk</code>), and <code class="language-plaintext highlighter-rouge">x-bypass-transform</code> to either <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code> depending on if we want to toggle the Worker changes on or off. You could then use <a href="https://developers.google.com/web/tools/lighthouse">Chrome Lighthouse</a> and run a performance audit with the changes both on and off. Using a tool like <a href="https://googlechrome.github.io/lighthouse-ci/viewer/">Lighthouse CI Diff</a> you could then compare the difference between the two sets of results.</p>
<h3 id="using-webpagetest">Using WebPageTest</h3>
<p>I use the browser method above for verification and debugging of the changes, but my preferred method of comparing results is using <a href="https://www.webpagetest.org/">WebPageTest</a>. It gives you incredibly detailed performance information that you can compare across multiple experiments. The test setup uses the <a href="https://github.com/WPO-Foundation/webpagetest-docs/blob/main/src/scripting.md#overridehost">overrideHost</a> scripting functionality to reroute any requests to the original domain through the Worker for modification. An example script I use for testing with GOV.UK can be found below:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>setCookie https://www.gov.uk/ cookies_policy={"essential":true,"settings":true,"usage":true,"campaigns":true}
setCookie https://www.gov.uk/ cookies_preferences_set=true
setCookie https://www.gov.uk/ global_bar_seen={"count":0,"version":8}
addHeader x-bypass-transform:false
overrideHost www.gov.uk govuk-worker.nooshu.workers.dev
navigate https://www.gov.uk/
</code></pre></div></div>
<p>Enter the above script into the <a href="/blog/2020/12/31/how-to-run-a-webpagetest-test/#script-tab">‘Script’ tab</a> of WebPageTest, <a href="/blog/2020/12/31/how-to-run-a-webpagetest-test/#advanced-testing-tab">configure all your test settings</a> then run a test. Once completed you can use the <a href="/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/#how-the-filmstrip-view-and-waterfall-chart-are-related">compare view functionality</a> to examine the difference between the different sets of results.</p>
<h2 id="summary">Summary</h2>
<p>In this post I’ve covered what Cloudflare Workers are, and how they can be used for various frontend performance tweaks and testing. Moving onto how you can easily check to see you are seeing the changes you expect, and then performance difference they make.</p>
<p>If you’ve watched <a href="https://twitter.com/csswizardry">Harry Roberts</a>’ talk <a href="https://www.youtube.com/watch?v=SVt7bjTwCMM">‘From Milliseconds to Millions’</a> where he talks about the importance of a pages <code class="language-plaintext highlighter-rouge"><head></code> tag for page performance, it’s now possible to easily try this. Using a Cloudflare Worker you have a way of testing changes quickly and easily against your production environment, without having to touch a single line of production code! Imagine being able to sell in performance work internally (or externally) with actual metrics you could see if production code <em>is</em> changed. Note: The results may not be 100% accurate compared to your production environment, but they are certainly a good way to give you an indication as to what is possible.</p>
<p>I personally think that’s a pretty great tool to have in your web performance toolkit!</p>
<p>I’d love to hear any feedback you have on the blog post above, or if there are any other recipes you use, please do <a href="https://twitter.com/TheRealNooshu">let me know</a>!</p>
<h2 id="further-reading">Further Reading</h2>
<p>There’s been more written about using Cloudflare Workers for web performance optimisation here:</p>
<ul>
<li><a href="https://andydavies.me/blog/2020/09/22/exploring-site-speed-optimisations-with-webpagetest-and-cloudflare-workers/">Exploring Site Speed Optimisations With WebPageTest and Cloudflare Workers</a> by <a href="https://twitter.com/andydavies">Andy Davies</a></li>
<li><a href="https://calendar.perfplanet.com/2019/prototyping-optimizations-with-cloudflare-workers-and-webpagetest/">Prototyping optimizations with Cloudflare Workers and WebPageTest</a> by <a href="https://twitter.com/dot_js">Andrew Galloni</a></li>
<li><a href="https://github.com/pmeenan/cf-workers">A collection of performance optimization scripts using Cloudflare Workers</a> by <a href="https://twitter.com/patmeenan">Patrick Meenan</a></li>
</ul>
<hr />
<p><strong>Post changelog:</strong></p>
<ul>
<li>02/03/21: Initial post published.</li>
<li>03/03/21: Added recipes for removing resource hints in the headers and HTML. Added table of contents.</li>
<li>14/03/21: Added link to my new blog post <a href="/blog/2021/03/14/setting-up-cloudflare-workers-for-web-performance-optimisation-and-testing/">‘Setting up Cloudflare Workers for web performance optimisation and testing’</a>.</li>
</ul>Matt HobbsCloudflare Workers are an awesome tool for rapid prototyping and observing web performance changes, here are a few recipes you may find useful.