Universal React: Potential, Performance, and a Presidential Campaign

Cheston Lee
Git out the vote
Published in
5 min readJul 19, 2016

On the fast-moving train of a presidential campaign you have to balance a lot of concerns. Development time, release engineering, team culture, performance, code quality, testing, automation… the list goes on. That isn’t even considering the fast-paced world of modern front-end development, which seems to shift with every week. At HFA we’re on the cutting edge, using tools like React and Flux, with code written in ES6 and beyond. But there’s more to engineering than using shiny new tools. We need to balance that against the fundamentals of supporting many high-traffic applications.

As an organization we’ve often opted for a static front-end driven by APIs. This is to help with scale, security, and speed. This architecture sets us up for an application entirely driven by JavaScript, since there is no user state in the rendered page at load time. As much as our campaign is a battle for voters, this is a battle for performance — what are all the eyeballs in the world if you don’t have content they can access quickly?

I work on the Donations engineering team which handles all of the donation pages for the campaign, along with other projects like shop.hillaryclinton.com. We launched a React/NuclearJS-based version of our application late last year, and shortly afterward we moved to packaging server renderings of our pages as part of our build process. We’d heard about the benefits of server-side rendering in React and how it could help our perceived performance, which was in rough shape at the time. We were experiencing a lot of ‘pop-in’ as components were rendered on the page, which left users anticipating when the form would actually look like something they could use.

In this blog post, I’m going to explore how we achieved server rendering with React, what the results looked like, and a little bit about why you may or may not want to implement this yourself.

Our front-end stack is fully JavaScript: we use React/NuclearJS for components and state management, Assemble for static site generation, Nunjucks for templating, and Webpack for module loading. With all of our dependencies in JavaScript, pre-rendering in Node sounded like a feasible plan. First, we take the template and render a page with a default data payload. Then, we hand that same data payload to our React application to render the component hierarchy to the DOM, along with a serialized copy of the Flux state from NuclearJS.

The static HTML & assets are built on the server.

When a user visits this page they’ll be served an already rendered and styled form! This, of course, isn’t the end of the story. After the initial page load, the application will rehydrate its Flux state from the serialized DOM copy, and then mix-in state from sources like query parameters, local storage, AJAX calls and experiment data. At the end of this process, the React application will spin up and re-attach itself to the DOM with the new and final state. This new state may modify the appearance of the form, but just giving the user what looks like a form in that second or so before the final state is ready gives the user a sense of progress, the sense that something is actually happening.

Flow of data through the application in the browser.
Look at that serialized Flux state, ready to load into our stores.

Voila! Now we’re in business. So what happened when we launched server rendering? Was it a success? Yes, sort of. Under ideal circumstances we saw a 9% improvement in time to first paint, and on 3G network conditions we saw up to a 40% improvement. This is a pretty great result, especially given that greater than 50% of donation traffic comes from mobile devices! Alas, not all is so simple, and there is a cost that comes with this gain. Namely, the time spent initializing the React application causes many re-renders, during which the page can be non-interactive. So while the application appears functional, it is actually locked up in running JavaScript. We’re currently blocking for 1.86s in JavaScript, preventing anything else from happening. For perspective, this is a >.5s or 37% increase over the non-server rendered version of the page. We are working to overcome these latencies, but they weren’t something that we fully understood when taking on the project.

😰

1.86s of blocking may be a small price to pay to decrease time to first paint as much as we did, but it depends on your application. However this was not the only trouble we ran into. In the beginning of the process, we had to root out any non-Node compatible dependencies in our application because we’re running all the application code as part of the server-side build. This meant substituting in new libraries, some without equivalent APIs, which comes with added risk. We also ran into issues with running React in the Node environment. React’s component lifecycle in Node is missing the `componentDidMount` event and so we had to make adjustments to behavior — it is now best used as a place to run browser-specific code. Since we utilize a static architecture, we also had to contend with the fact that the final state on the client would be different than that which was rendered on the server. React is generally not very happy about this and throws when the state is updated before the component calls `didMount`. We work around this by bootstrapping the component with the same state that it had during server rendering, and then by updating the state with query parameters, local storage and experiment data. This causes more renders of the application, but it keeps React happy and the initial client render fast.

As you can see, this project has revealed a lot of nuance to a complicated problem. While we were able to address our primary concern of perceived performance, we introduced complexity and a hiccup of lag. When you have the attention of a nation and your mission is to get your message across, performance is paramount. It’s our job to make sure we keep delivering Hillary’s message before folks get bored and load a cat gif. Want to work on hard problems and help elect the next President of the United States? Come checkout our jobs page.

P.S. Shout out to the folks at Assemble, who have been very responsive to our needs, and to the maintainers of all the open source projects that power our applications.

--

--

Cheston Lee
Git out the vote

Engineering leadership, AppSec, US Politics and cats.