Layer0 is a major contributor to the open-source eCommerce PWA framework, React Storefront. Earlier this year, we contributed many new features and optimizations as React Storefront v7, two of the most significant were the shift to Next.js and the removal of several key dependencies (e.g. MobX) in favor of React’s newer built-in capabilities for managing state, such as the useState hook and the context API. These resulted in the browser bundle size being cut roughly in half.
This was a nice gain at the time and helped bump Lighthouse (v5.7) performance scores for typical React Storefront apps from the 80s into the 90s as measured by PageSpeed Insights (PSI). For context, a score of 83+ outperformed 99% of the top 500 eCommerce websites on Lighthouse v5.7. We didn’t realize how essential the bundle reduction would prove in the coming months, when Lighthouse v6.0 would drop like a bomb and obliterate everyone’s performance scores.
See how the distribution of Lighthouse scores measured on PSI for the leading eCommerce websites changed when v6.0 dropped:
In this post, we share how we improved Lighthouse v6.0 scores for React Storefront, but the techniques can be applied to other frameworks.
Also, it’s important to note that Google announced on May 28th, 2020, the specific metrics they will use to rank sites in 2021. The Lighthouse performance score will not be used, although some elements used to determine that score will, and even then, they won’t be measured using a synthetic test such as Lighthouse but rather real-world field data from the Chrome User Experience Report (CrUX).
Lighthouse v6.0 introduces several new perceptual speed metrics and reformulates how overall metrics affect a page’s Lighthouse performance score.
First Contentful Paint (FCP)
First Contentful Paint (FCP)
Speed Index (SI)
Speed Index (SI)
First Meaningful Paint (FMP)
Largest Contenful Paint (LCP)
First CPU Idle (FCI)
Total Blocking Time (TBT)
Time to Interactive (TTI)
Time to Interactive (TTI)
Cumulative Layout Shift (CLS)
If you’re using an isomorphic framework, like Next.js, which supports server-side rendering, TBT is mostly determined by bundle size and hydration time. Put simply, the only way to improve TBT is to remove dependencies, optimize your components, or make your site simpler by using fewer components.
LCP is a new metric that has a weight of 25% over the overall Lighthouse v6.0 score. LCP aims to quantify how the user perceives initial page load performance by observing how long the largest contentful element takes to finish painting. For most sites, especially in eCommerce websites, the largest contentful element is the hero image. In the case of product pages in React Storefront apps, the largest contentful element is the main product image. If you’re unsure which element this is on your site, PSI will tell you:
To optimize for LCP, you must ensure that the image loads as quickly as possible.
Cumulative layout shift measures how much the page layout shifts during the initial page load. Layout shift is most commonly caused by images, which tend to push the elements around them as they resize to accommodate the image once data is downloaded from the network. Layout shift can often be fully eliminated by reserving space for each image before it loads. Fortunately, React Storefront’s Image component already does this, so the React Storefront starter app boasts a perfect CLS score of 0 out of the box.
It should be noted that other common culprits of poor CLS are banners and popups that appear after the page is initially painted. Users hate them and now Lighthouse does too.
When we first tested the React Storefront starter app’s product page on Lighthouse v6.0, using PageSpeed Insights, it scored in the low 60s:
To improve the score, we first set our sights on LCP. At 2.5 seconds, FCP was worryingly high (we’ll get to that later), but the nearly 3-second gap between FCP and LCP stood out as something that needed improvement.
And here’s the code that fetches and converts the image to base 64:
Pretty simple and old school. And the impact on the score?
By dropping the LCP from 5.3s to 2.8s, we gained 21 points in the page’s Lighthouse v6.0 score! It’s a bit unsettling how such a small change can dramatically affect Lighthouse v6.0 score, but we’ll take it. It should be noted that all of the metrics vary somewhat between runs, but the overall score was consistently in the low 80s. For context, the highest performing leading eCommerce website on v6.0 scores 87 as measured on PSI and looks like it’s straight out of the 90s- take a look www.rockauto.com
The gap between FCP and LCP showed above was about as large as we saw it across several runs. Most times the gap was in the 100ms to 300ms range. Occasionally FCP and LCP were the same.
Fortunately, the React Storefront community had already begun work on supporting lazy hydration before Lighthouse v6.0 dropped. This certainly made us accelerate our efforts.
In case you’re unaware, hydration refers to React taking control of HTML elements rendered on the server so that they can become interactive. Buttons become clickable; carousels become swipeable, etc. The more components a page has, the longer hydration takes. Complex components, such as the main menu and the product image carousel, take even longer.
Lazy hydration entails delaying the hydration of certain components until it is absolutely necessary and most importantly, after the initial page load (and after TBT is calculated). Lazy hydration can be risky. You must ensure that page elements are ready to respond to user input before the user attempts to interact with them.
Implementing lazy hydration on React Storefront proved quite difficult due to Material UI’s reliance on CSS-in-JS, which dynamically adds styles to the document only after components are hydrated. I’ll save the details for another time. In the end, we built a LazyHydrate component that developers can insert anywhere in the component tree to delay hydration until a specific event occurs, such as the element being scrolled into the viewport or the user touching the screen.
Here’s an example where we lazy hydrate the MediaCarousel that displays the main product images:
We applied lazy hydration to several areas of the application, most notably:
The slide-in menu: we hydrate this when the user taps the hamburger button.
All of the controls below the fold: include the size, color, and quantity selectors, as well as product information tabs.
The main image carousel: this and the main menu are probably the components with the most functionality and, therefore the most expensive to hydrate.
Here is the Lighthouse v6.0 score with lazy hydration applied:
Lazy hydration cut TBT by nearly 40% and trimmed TTI (which has a 15% weight over scores in v6.0) by 700ms. This netted a 6-point gain in the overall Lighthouse v6.0 score.
You’ll notice FCP went up a bit, but LCP went down. These small changes are essentially “within the noise” you get when running PageSpeed Insights. All the scores fluctuate slightly between runs.
Based on the score above, we felt that FCP and/or LCP might be further improved. We know that scripts can block rendering, so we looked at how Next.js imports scripts into the document:
Using async here might not be the best choice. If the script is downloaded during rendering, it can pause rendering while the script is evaluated, which increases both FCP and LCP times. Using defer instead of async would ensure that scripts are only evaluated after the document is painted.
Unfortunately, Next.js doesn’t allow you to change how scripts are imported, so we needed to extend the NextScript component as follows:
Then we added the following to pages/_document.js:
To our delight, this did improve the LCP and overall scores:
It also slightly bumped the FCP on many runs, but this may be within the “noise.” Nevertheless, the overall score was consistently 2-3 points higher when using defer vs async.
When Lighthouse v6.0 was released in late May 2020 the performance scores for many sites, including React Storefront apps, plummeted. Before optimizations, the React Storefront starter app’s PDP performance was mired in the low 60s. With these optimizations, we got it into the now-rarified air of the low 90s. At this point, we think the only way to further improve the score would be to remove dependencies, which may mean trading off developer productivity for application performance.
That’s a discussion for another time. Let me leave you with some things we tried that didn’t work:
Preact makes the bundle size 20-30% smaller, but Lighthouse v6.0 scores were consistently worse across all metrics, even TTI. We have no idea why, but we know this is not new or exclusive to Lighthouse v6.0. It was slower with Lighthouse v5.7 as well. We continue to check in periodically and hope someday this is fixed.
Next.js recently introduced finer-grained chunking of browser assets. When this was first introduced in Next.js 9.1, we noticed that the additional, smaller chunks actually made TTI worse. It probably makes the app faster for returning users after a new version is released because it can better leverage the browser cache, but Lighthouse doesn’t care about any of that. So React Storefront has limited the number of browser chunks to one for a while:
Most sites use a custom web font. By default, React Storefront uses Roboto (though this can be changed or removed). Custom web fonts kill performance, plain and simple. Remove the web font and you’ll gain about 1 second of FCP.
As with analytics, stakeholders seem willing to trade off performance to have a specific font. React Storefront serves the web font from the same origin as the site to eliminate the TLS negotiation time you’d incur if you loaded the font from a third-party CDN, such as Google Fonts. Specifically, we use the typeface-roboto package from NPM. When combined with Webpack’s css-loader , using typeface-roboto results in the font being imported via a separate CSS file which the browser needs to download and parse. We thought that inlining that CSS into the document might help with performance, but it didn’t.
When it comes to performance, you always need to measure. Optimizations that should work in theory may not be in practice.
Our customers rank in the 95th percentile of the top 500 eCommerce websites on Lighthouse v6.0.
Get the information you need. When you’re ready, chat with us, get an assessment or start your free trial.