Airbnb uses React Router and Hypernova to Harvest Low-Hanging Performance Fruit
After successfully migrating the site’s landing page and search results, it was time to tackle the core of the Airbnb booking flow: the listing detail page. This is where users view expanded information on and photos of specific interesting rental possibilities, and ultimately click to close the deal. As such, it’s one of the most critical pages on airbnb.com — and slow or janky page behavior can torpedo the booking.
The company tasked front end/web infrastructure engineer Joe Lencioni with optimizing the new SPA architecture for the core booking flow, and specifically the listing detail page. Lencioni spoke with The New Stack to share the inspirations and approaches that led him to successfully mine for “low-hanging fruit” to enhance React performance while reliably connecting visitors with their dream vacation lodgings.
What about Airbnb’s site flow made moving to a single page app the most effective option?
For Airbnb though, people need to come in from all kinds of entry points — we can’t just ship everything down in one chunk and say, ‘There you go, thanks for waiting, now everything’s fast!’ We have to get it as fast as possible for as many different entry points as possible. So server rendering becomes a big part of the setup, because you could be landing on any page. Making it possible that you can click the links before the JS is even parsed and executed, that is our goal.
Even outside the context of SPA or progressive web apps, server rendered React can help so much. So to harness that we’ve been building an open source project called Hypernova, a node service you can send React components to that will render it and give HTML back. And then Rails will put that in the Rails template and deliver it.
Were there any debates involved with server rendering?
There is always tension between how much should you server render, and why. Too much, you lose performance, and too little you lose on the SEO. SEO can be a serious consideration depending on the market you’re targeting. You can calibrate by considering your goal for a given page. Obviously, bots aren’t going to be logging in so you can make a different tradeoff for authenticated requests. It’s a balancing act you’re always playing around with.
I personally land in the general area of server rendering enough to make the page reasonable above the fold — make it feel like something is happening as quickly as possible, and then make it interactive as quick as you can after that. Worry about search results later, once you understand the page and what’s going on inside.
Airbnb’s goal is for pages to start fast and stay fast.
Service worker and service shell architecture play well because we can server render our app shell. Think of airbnb.com as a set of related but separate SPAs. For example, imagine a guest finding a place to book as a flow — people entering that app will be navigating there almost exclusively. But if you are a person who wants to list your new space on Airbnb, that is a whole separate app. Or a host who wants to manage your calendar, that is its own app. So what if each has its own app shell, so the service worker can be set up so that even if you exit the app when you come back it can be re-rendered quickly.
Let’s hear about that low-hanging fruit!
Airbnb’s goal is for pages to start fast and stay fast. Once the booking flow restructuring was in place, we went through a process of profiling interactions on the page — scrolling, clicking, typing — then making a fix, then profiling again. We were able to dramatically improve the interaction performance on the listings detail page to make the booking experience smoother and more satisfying for our users.
It was more about small but significant tweaks than big dramatic changes. As I was working on this page, for example, the scrolling animation was extremely slow. So I dug into it and found all these little things that added up to a less than stellar experience, and the more I poked around the more I found.
As another example, in my profiling, I discovered that every key press was causing the entire review section header, and every review under it, to be re-rendered. To fix this I extracted part of the header to be its own component so I could make it a React.PureComponent, and then sprinkled in a few React.PureComponents throughout the tree. This made it so each key press only re-rendered the component that needed to be re-rendered: the input.
There is a lot of very detailed information in the blog post about how I tracked things down and what I did about each of them. Plus lots of pretty flame graphs!
And profiling is how a dev goes about harvesting it?
I’ve become almost an evangelist for “Profile, make a change, and then profile again.” That is where you reap the results. If I am an engineer making changes to a page, the feedback loop for me to understand the performance impact of a code change I’m making is a broad and complex thing that is often too expensive for me, time-wise, to stop to figure out. And we are so often under pressure to just get things working and out the door that profiling is something we worry about later, maybe, if we ever get time. Which we often don’t.
But we have a tool right in the Chrome dev tools: the performance tab is super useful for this. And most of the devs I talk to have never used it, even though it’s right there. But I get why — it can be intimidating if you’ve never used it before. Once you can kind of lead people through, though, they can see for themselves what a powerful tool profiling can be.
The workflow is pretty basic: open the page in an incognito window, so browser extensions don’t interfere with the profiling. Then visit the page in local development with ?react_per in the query string, to enable React’s User Timing annotations. Then you click the record button, interact with the page — scroll, click, type — and then click the record button again to stop. Then interpret and compare your results.
Any takeaway advice for devs throughout the industry?
So how from the performance side do you even make sense of that?
So my number one piece of advice is to profile as often as you can.
Maybe not every day, that’s not realistic, but at least once a month — profile your stuff. Get used to the tools, spend an hour every now and then learning something new with dev tools and profiling and interpreting the results. Eventually, you can start seeing patterns in the code, and start to avoid them instinctively. Like, I have a huge array here, maybe I should not iterate over it 45 times, or in react think about where my peer components should be and do I really need to pass this prop? Understanding how your app performs in the real world as it’s put together with production data. And the only way to do that is by profiling.
Feature image via Pixabay.