A Small Web Performance Fix with a Big Impact

--

A website’s performance — how quickly and smoothly it loads and responds to interactions — is a critical aspect of user experience. We take web performance seriously at Reverb, and this post describes one case where a seemingly minor change resulted in a big performance boost for our users.

The redesigned Reverb.com category menu

As the largest online marketplace dedicated to buying and selling new, used, and vintage musical instruments, showcasing Reverb’s hundreds of product categories is a challenging but incredibly important part of our site design. To that end, we recently redesigned the expanding category menus in Reverb’s page header.

Since we wanted all the links in the menus to be visible to search crawlers, we decided to render the full HTML for all seven categories even when they’re hidden with display: none. This is a common SEO practice for expanding menus, but this decision had negative consequences for page performance that we didn’t realize when we first rolled out the new design.

The Problem

The category menus have a lot of content — about 300 links and over 800 DOM nodes across seven individual menu panes. A typical page on Reverb.com has about 2000–2500 DOM nodes, so this is a significant portion of our HTML for a component that only a small percentage of users will actually see.

That extra HTML increases latency; rendering on the server, transferring over the network, and parsing in the browser all take more time if there’s more HTML. But there was another performance problem that this SEO-friendly behavior had caused:

Screenshot of browser dev tools network inspector, showing the seven hidden category images before the main images on the page
Timeline of images downloaded while loading the Reverb.com homepage

Each of the seven menus has an <img> tag, so even though they were being hidden with CSS, the browser would still see these image tags in the HTML and download all seven images.

To make matters worse, the HTML for these category menus is near the beginning of the document, before the main section of each page. In the timeline above, the browser was smart enough to defer downloading these hidden images (highlighted in orange) until after it started downloading the main HomepageHero image. But they were still competing for bandwidth with the first images a user would see on the page, prolonging the time until the page appears fully loaded.

The Fix

Most of our frontend is built with React and Typescript, and we render components server-side with a Node service that handles requests via our Rails monolith. This architecture has served us well for many years, and made it fairly straightforward to fix this issue once we discovered it. In our CategoryFlyout React component, we use a boolean visible prop to control whether that menu should be displayed. Originally, we used this prop to append a category-flyout--visible modifier class to the top-level element, which changes the display style from none to block. We still wanted to use this approach when a visitor’s User-Agent header matches our list of known bots, so that search crawlers still have access to the full (hidden) HTML. But for typical users, we now return an empty render when visible is false, omitting the HTML entirely.

Here’s an abbreviated version of our CategoryFlyout React component, and the change that we made. First the original component:

And with the optimization:

To a user viewing the page, the behavior is identical, now there’s just a lot less HTML when the CategoryFlyout is closed!

Testing the Performance Impact with Lighthouse

Modern web apps and browsers are complex, and it’s not always obvious how a change will affect real-world performance. We wanted to validate that this optimization would, in fact, improve page performance, so we deployed the optimization along with some logic so that it could be toggled with a URL query parameter. For this initial test, the original un-optimized behavior was still the default. But by appending ?be_fast=true to the end of any URL, we could enable the optimization. Then we used Lighthouse—a page auditing tool from Google that’s built into Chrome’s dev tools — to compare performance with and without the change.

Lighthouse attempts to provide consistent results by controlling for things like network speed, but there will always be some random variation from run-to-run. To control for that, we ran the test multiple times on a set of item page URLs and averaged the results.

Our hypothesis was that our optimization would reduce Largest Contentful Paint times. LCP is a common metric for perceived page load speed. It measures the time from the start of the page load until the browser renders the largest element on the page.

Average LCP, Lighthouse mobile performance audit

  • Before: 19.1s
  • After: 18.46s
  • Change: -0.64s (-3.35%)

In almost every individual trial, the optimized version was faster than the default. Averaging across eight trials each for the default and optimized versions, the new one was over 600ms faster, an improvement of 3.35%. It’s important to note that the LCP values here aren’t comparable to load times for real users because Lighthouse simulates very slow network and CPU speeds — typical users see the page load in much less than 18 seconds! But the positive impact of the change was clear, at least in an isolated test setting.

Measuring Impact Among Real Users

The last step was validating that our optimization actually improved page performance for real users. We’ve recently started using Google’s web-vitals library to collect frontend performance stats for a sampling of users. This allows us to track metrics like LCP at a high level so we can detect any performance regressions that might make it into production. We toggled our optimization once a day to see if we could detect variations in this — often quite noisy — data.

Graphs showing LCP and FID over time as the optimization is toggled off and on. Both metrics are noticeably lower with the optimization on, espeically FID on mobile
LCP and FID over time

And sure enough, we could! The LCP for mobile web page views went from an average of 3.71 seconds before the change, to 3.27 seconds afterward.

Even more impressive was the impact on First Input Delay, a measure of responsiveness when a user first interacts with the page by tapping or focusing an input field. Before the change, our average for mobile FID was around 230 milliseconds, long enough to result in a noticeable lag for users. When we turned on this optimization the average FID dropped to about 130 milliseconds — more than a 40% improvement!

Conclusion

This was the first time we looked so closely at the impact of a performance fix in a “lab testing” environment like Lighthouse, and then validated it with data from real users. We’re continuing to learn new ways to use these tools to make Reverb.com even faster and more responsive.

Are you looking for a new challenge and intrigued by the way we work at Reverb? We’re hiring and It’s an incredible time to join our fast-growing musical instruments marketplace! Visit Reverb Careers to see open positions and learn more about our team, values, and benefits.

--

--