Gatsby sites are known for being fast and being fast in every way that matters. Using what Gatsby offers out of the box has given some baffled folks cause to take to Reddit trying to figure out how Gatsby sites load so immediately.
Of the many ways you can measure performance on the web, be it initial page load time, runtime efficiency of a web app, or server response times, one of the most well-known is the Lighthouse score. It’s a score from 0-100 that aggregates weighted scores from metrics defined by Google known as web vitals. Because higher Lighthouse scores rank higher in Google search results as of 2021, and a higher score means a better experience for the user—both worthy causes—we set out to improve our own Lighthouse score on Gatsbyjs.com.
The Lighthouse score isn’t a perfect measure of user experience for all users of a site, but with it as our measuring stick, we improved the scores we were receiving by 22.8 points.
What are we measuring?
There’s wisdom in the carpenter’s proverb: “measure twice, cut once.” Before starting, it was important to identify what we were measuring. Knowing how the Lighthouse score is calculated informed us on where to focus effort.
Lighthouse links to a scoring calculator from every report. Here’s a screenshot of one of our first runs:
We only scored a 45 when testing a mobile device, not exactly something to write home about. Looking at this chart, it’s very clear that Time to Interactive (TTI) and Total Blocking Time (TBT) were our biggest offenders to our score. We were only scoring 6 of 40 possible points for them.
Other metrics were already performing well for us. Thus, they were a lower priority to optimize even though there may have been slight room for improvement. For Cumulative Layout Shift (CLS) we were already scoring 5 of 5 possible points, so more optimizations in that area wouldn’t improve the user experience or Lighthouse score any more.
Overview of the Process
To actually go about exacting changes, we roughly followed these steps:
- Measure the baseline branch
- Investigate performance audits for possible optimizations
- Create a hypothesis (or experiment) and deploy a preview branch to compare against
- Measure the preview branch
- Review, merge, rinse, repeat
Testing Methodology
Controlling for as many variables as possible is the best way to get accurate comparisons between experiments. We used WebPageTest to test each experiment because it uses real phones in a carefully calibrated network environment, meaning the results for web vitals are often more consistent than you’d get testing with other tools.
Instead of looking solely at the Lighthouse score, we recorded the web vitals from the runs in a spreadsheet to create averages. To measure each experiment we’d run the site through WebPageTest 15+ times.
Experiments
The following experiments were attempts we made to improve different metrics. Some had more of an effect than others because they targeted improvements to different web vitals.
Remove re-layouts
Some of our rendered code on our page positioned elements with elements based on distances calculated with getBoundingClientRect
. This allows us to create animations based on distances from other elements, but it would cause the browser to paint or layout the page multiple times. By wrapping these in requestAnimationFrame
calls that tell the browser when something is going to animate, we could prevent the redundant work by the browser.
Although this is pseudo code, it looked somewhat like this:
This change didn’t significantly improve our Lighthouse score yet because we were still receiving slow scores on TTI and TBT. It did help bring those metrics closer to the threshold where we’d start seeing them improve our Lighthouse score.
Font loading
Fonts being loaded as an external resource can be expensive traveling over the network and can also create flashes of unstyled content when text renders without a font being available. The network issue can be especially detrimental to time to interactive, and the flash of styles can hurt cumulative layout shift. In some cases, a font loading can push text to flow onto additional lines, and then shift back.
We added preload
to our link tag pulling in our font so it looks like this:
There’s a difference between preload
and prefetch
, in that prefetch
will load an asset for a page you are going to visit, not the current page, so preload
is what aids the initial page load. These attributes are hints for the browser, and the browser can do what it wants with them.
This experiment made us consistently score perfectly for CLS, dropped TTI and TBT, and bumped our lighthouse scores up 5-7 points.
Removing excess dependencies and code
We used gatsby-plugin-webpack-bundle-analyser-v2 to check our bundle for code that was loaded but not used. Reducing our bundle size gives the browser less to download over the network. We also made decisions to consolidate marketing scripts that can particularly slow and block the main thread if not loaded async
.
We deferred loading marketing scripts until after the client side app has been hydrated (with the built in onInitialClientRender
Gatsby Browser API) which helped greatly reduce TTI. These changes bumped our scores up another 5-7 points.
For example, with Google Tag Manager, rather than initializing the script early on the browser’s parsing of the HTML content, we wait until React has hydrated:
Disabling offscreen animations
By using a helpful open source React hook called react-intersection-observer-hook, we were able to use an intersectionObserver to turn animations on and off based on when they were in the viewport—once again, giving the browser less to do.
In some places, we were using computationally expensive CSS properties that force more work for the browser like transition: all
and lots of transforms
which can add up. By applying the CSS will-change
property selectively we were able to inform the browser of these ahead of time and let the browser defer to the computer’s GPU to handle. We also replaced instances of translateX
to use transform3d
for the same benefit.
We attributed a slight decrease in TBT to these changes, though it was hard to measure precisely.
The content-visibility property
Touted as “one of the most impactful new CSS properties for improving page load performance”, we were excited to try the [content-visibility
property](https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility). It tells the browser not to render certain chunks of the page until they reach the viewport. It’s similar to hiding them with an intersection observer but does a lot of the work for you. You add two properties:
We were disappointed when we started measuring the effects on our own site, because it didn’t appear to make any difference in the numbers for core web vitals we were seeing. Acknowledging that there were also some accessibility concerns with this method we’d need to account for, we chalked it up as a failed experiment.
What’s Next
We’re looking forward to improvements in the React framework that can eventually help us hydrate only essential pieces of our longer pages without blocking the JavaScript for too long. Hydration of the DOM tended to be one of the most computationally expensive tasks for the browser in our tests. By limiting the page size we could possibly score higher, but we opted to keep the content we think is valuable on the homepage.
Improvements to Gatsby and the ecosystem
We set out with the goal of improving the Gatsby framework as a whole and using what we learned from our site to build solutions back into Gatsby core. We plan to add an option to the Google Analytics and Tag Manager plugins that could optionally let you defer the initialization of these scripts to later in the lifecycle to help with TTI. This comes with a few tradeoffs, but an option to enable it seems appropriate.
Research into our own site and what we could do to reduce the overall JavaScript on the page spawned discussions on code splitting and better bundling in Gatsby core. The release of webpack 5 in October 2020 made a good case for upgrading the version of webpack used in the Gatsby core framework. When we upgraded our site to use Gatsby v3 with the new version of webpack we saw a reduction of 32% (328kb → 236kb) in total kilobytes loaded and a reduction of 36% (296kb → 205kb) in the amount of JavaScript loaded in the homepage, without making any additional changes.
Here were the specific improvements in our case:
The Bottom Line
We still have room to improve the performance of our own homepage—we’re not scoring a perfect 100 unless you’re coming to the site from a desktop or on a good network. Though the vast majority of the visitors to gatsbyjs.com are viewing from a desktop with good network speeds, we think we can still do much better. We’ve had some hacky ideas like lazy loading chunks of content but haven’t arrived at a solution that seems to be worth the tradeoffs yet.
Our website is a complicated unified site that stitches together multiple specific CMSs with a full-scale React application. We want to continue to make our site a shining example of the powerful applications that can be built on top of Gatsby.
In addition to optimizing our own website, we want the learnings from an exercise like to this to help other Gatsby sites in the wild be as fast as possible. It’s optimizations like these that feed into the core framework and help improve the 200,000+ sites already using Gatsby.
If you’d like help with performance on your own site, the Gatsby Concierge service offers specialized help on your own Gatsby sites. Talk to our team of experts to supercharge your website performance.