arielrobin (Public Domain) https://pixabay.com/en/measure-yardstick-tape-ruler-1509707/

ReactJS SSR Profiling and Caching

Joel Chen
Walmart Global Tech Blog
8 min readSep 7, 2016

--

Introduction

At Walmart, we’ve been busy migrating our eCommerce sites to use ReactJS. With its declarative and component-based approach to creating interactive UIs, our developers have been responding very positively to this adoption.

Since our sites are mainly for eCommerce, SEO is an important requirement, which means returning HTML from the server. With built-in server side rendering (SSR), ReactJS allows us to build isomorphic code that can be rendered on both the server and the client. Unfortunately, one downside of SSR is a huge performance hit on complex components, and we’ve been working at improving SSR time.

gave an excellent presentation with tips and tricks for speeding up SSR. After applying all of his tips, the performance improved substantially, but we still see our server spending up to over 100 milliseconds in the synchronous renderToString method. Sasha also gave a quick demo of component caching that reduced the time to single digits. Even though caching can be tricky, to reduce our SSR time further, we turned to caching.

We have multiple developers working on different aspects of SSR performance and caching. In this blog I will go into specific details and write about my experience with SSR component caching, performance profiling, creating custom cache profiles and their verifications, and component cache-ability.

SSR Component Caching

The idea is straightforward. In Sasha’s version, since he forked and patched React’s code to do streaming, he handled caching in the mountComponentAsync method for ReactCompositeComponent.

Since we are using the original React module, we can directly patch ReactCompositeComponent to wrap its mountComponent method, as demonstrated in the code below.

Patch ReactCompositeComponent to do component caching for React SSR.

Next, we need strategies to generate a cache key. There are two approaches: “simple” and “template”. There is technically a third approach which could be called “custom.” It is basically allowing each component to provide a custom function that returns the cache key, which I won’t go into in detail.

The simple strategy is to just hash the props directly. This works well for components that have few variants, like our Header and Footer components, but not for more dynamic components.

For example, every item on the Walmart.com website has its own properties such as name and price. If we cache all of them, then the cache size would be too big, since we have millions of items.

Our collections and category pages list many items on a single page and they need SSR. Since each item takes a few milliseconds to render, on some of the big collections page, the SSR time could be 200 milliseconds or more.

To solve this, we generate our cache key by templating the props.

Templating Component Props

The “template” strategy is to replace the prop values with special tokens instead of using the original props directly. With the template props, we create the cache key using hash, and then render the component. React SSR renderToString returns the HTML string with these special tokens inside, which can be considered a template HTML. As long as a component renders the same HTML structure with templates or actual props, we would be able to cache and reuse the template HTML. We just need to use string replacement to change the tokens to the real values. I think of it as generating something like logic-less handlebars templates from React components.

For example, below is a capture of one of our collections pages with a product card highlighted in red.

If we replace the prices and title with tokens like {1}, {2}, {3}, etc., then we could get the HTML template for a product card that might look like the image below.

The next image is a code sample on what turning a props into a template might look like. Note, that instead of using the string JSON paths as values in the template props, it uses one more indirection to have a lookup table to refer back to the JSON path to get the original values from props.

The lookup table adds more assurance that the string JSON path won’t be affected in the generated HTML template, which would be smaller and cleaner. It also allows the JSON path to be kept as an array instead of as a string. Below is a sample of the process.

A sample of a template for a props. When calling the original mountComponent, template is passed in instead of props.

So, those are the two caching strategies. Some components work well with one and some work better with the other.

In order to improve SSR performance with caching, I first need to figure out which components can be cached and the strategy to use. To do that, I run some profiling on the SSR first.

SSR Performance Profiling

If you have components that are good candidates for the “simple” strategy, they should be fairly obvious to pick out. We don’t have a lot of them, but things like Header and Footer on our pages are good ones.

Since the “template” strategy requires some processing to generate the template and restoring the values, it’s best if we use it with the more expensive components. To find out the components that take the longest to render, we can track their rendering time in the patched mountComponent method, as demonstrated in the code below.

Wrap mountComponent to collect render timing for composite component.

I would run SSR on a collection data with the above code once, but I would do it a few times with profiling turned off first, so the V8 engine optimization warms up. After running SSR on a collection data, I collected the timing information and logged the data to a file. Below is a partial sample of that data in YAML format.

A partial sample of the profileData collect with our collections page.

With this data, I picked a few components to develop a custom template profile to test caching them.

Custom Cache Profiles and Verification

To come up with the data to apply the “template” strategy to cache some of the composite components, in my full profiling code, I also capture and save the props. With this data, I experimented with caching some of the expensive components, and found a few cache profiles that work well. Even though many components don’t work with caching, SSR time improved on average by 50% or more for our product collection component on all the collection data.

In order to find the components that are good candidates for template strategy caching, I add automatic verification code in my caching code. The code would do a render with the real props and then lookup the the cached version. If they match, then the component can be cached. Of course very few (in fact, none) match initially. I manually save the HTML strings and compare them using kDiff3 to fine tune the hashing for individual components. When fine tuning, a technique I apply to both caching strategies, is to manually analyze the component props to identify and omit some keys that wouldn’t affect the render output.

To verify that caching worked properly, I downloaded all of the collections data from our DB and rendered all of them with and without caching. Then I compare the results to make sure they are the same. To make the comparison work, I remove all the data-reactid and data-react-checksum attributes from the HTML string first.

Some issues I found with string values are that the component may apply encoding to the strings, but not always. One example of this is when the string values are used as attribute values in HTML tags and if the string contains single or double quotes, &, <, and >, then they are encoded. Another case is when the value is an URL, the component would remove any http: or https: prefix from them. For these, when restoring the prop values, I detect them and format the values also.

Still, many components are simply not cache-able. It’s because their rendering logics heavily depend on the actual property values. I call this “component cache-ability,” which needs to improve for further performance gain on our collection pages with caching.

Component Cache-ability

Out of the box, most components are not good candidates for caching. If a component props contains the children object, we avoid caching them.

Components with props that have large variations, are not good for the “simple” strategy, and may be candidates for the “template” strategy, which also has limitations on what components can be applied.

For the “template” strategy, some basic reasons a component cannot be cached are:

It’s hard (or impossible) to template non-string props since the code are more likely to have logic that depend on those values. Such as two execution paths based on a Boolean value, or a loop base on a number with different values. Even with strings props, the code could behave differently base on the value. For example, collection status could be “PUBLISHED” or “UNPUBLISHED.” Also, the code could apply formatting to string values.

If a complex component mixes prop value dependent logic with other rendering code, then the entire component is not cache-able. For example, we have a component which will enable displaying stars based on customer reviews of a product. The component displays a half star for any partial decimal value above 0.4. There are about 10 different render outputs based on HTML structure. It’d be a good candidate for the “simple” strategy, but it’s not cache-able since the average review value is a decimal and there is a large variation. It’s significant to the component logic that decides whether to display a half star.

This component’s props has only two properties: totalStars and averageRating. To make this component cache-able with the “simple” strategy, we can do the calculation on averageRating to find the number of whole stars and partial stars first, and then we can separate the expensive rendering logic into another component that’s easily cache-able.

Now, we have a new Rating component that’s still not cache-able but with light logic. The Stars component with the expensive rendering logic is now cache-able with the “simple” strategy, since avgFloor is an integer in the range of 1–5 (if totalStars == 5) with isPartial being true or false only.

A very complex component we have that should’ve been a good candidate for the “template” strategy is the product card component. It displays the product with an image, the product name, and the prices. The image URL and product name can easily be turned into a template, but not the prices, because the component breaks the price into dollar and cents amount, and displays them with different styles. If we create a new component that contains the logic to separate the prices first, then the rendering component would just do display only, which is then cache-able with the “template” strategy.

Conclusions

ReactJS offered a refreshingly new approach to web development with isomorphic support, but the synchronous server side rendering performance is an issue with complex components. We are improving that with component caching.

Any kind of caching on web components could be dangerous and tricky. If there is personalization with user specific information on the page, then extreme care should be taken with caching. For an eCommerce site like Walmart.com, where we display millions of products in similar formats, component caching is helpful.

Still, caching is tricky and requires profiling to apply different caching strategy and profiles for different components. Component cache-ability is important and it helps to write components with that in mind.

Other News and Updates

Thank you for reading. If you’d like to learn more about what we are doing, please check out our other posts such as Building ReactJS at Enterprise Scale.

Big thanks to:

, (Twitter: @lexgrigoryan), and (Twitter: @nanavatiarpan).

--

--