Analysis / Case Study / Interviews / Technology / Top Stories

Airbnb uses React Router and Hypernova to Harvest Low-Hanging Performance Fruit

5 Dec 2017 6:00am, by

Just over a year ago, the person-to-person vacation rental site Airbnb began taking steps to accelerate its booking services via a more efficient stack setup. The idea: build a single-page, server-rendered app powered by React Router and Hypernova, Airbnb’s own open-source service for server-side rendering of JavaScript views. Exploiting progressive web apps harnessed to service worker scripts within an app shell architecture promised a potentially serious performance upgrade, with fast interactions and smooth transitions even for users in exotic but internet-challenged areas like, say, the beaches of Bali.

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?

In the past, a lot of SPAs are not server rendered at all, and the typical experience you get is a long initial load because it’s downloading all the JavaScript it needs at one go. And this is great for certain kinds of pages because once you’re in, everything is really fast — it’s all AJAX, no worries about entry points or SEOs in, say, Gmail.

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.

Using Webpack and code splitting and React Router, we are code splitting out SPA based on route and component, based on what we are trying to optimize for bundle size. After a page is downloaded, we continue to download JavaScript so that, for example, if you have hit our front page and then after a few seconds want to hit the search button, that JavaScript has been pre-fetched to make the transition super fast. We haven’t landed the perfect solution yet, but we are iterating on it. Our biggest problem for performance is basically that we ship too much JavaScript to clients.

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.

Before: 61.32 ms.

After: 3.18ms

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?

In the JavaScript community, things are changing so fast — there’s all this new stuff, but also all this old stuff, and we have to somehow get everything all working together.

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.


A digest of the week’s most important stories & analyses.

View / Add Comments

Please stay on topic and be respectful of others. Review our Terms of Use.