Key Takeaways
- Using React by itself will not result in a highly performant application. If you’re not careful, the application can pick up bloat easily. It’s good practice to conduct audits periodically.
- Chrome DevTools offers powerful performance measurement for JavaScript applications. Learn how to read and understand performance profiles.
- Code splitting is easy to setup with Webpack 4 and you should definitely use it to optimize your application.
- Identify where long lists of content are rendered and optimize them with react-window.
- Understand how React works internally. This will help you identify wasted renders and fix them. Use the “Highlight Updates” option in React DevTools to help you with this step.
- There are several other methods to improve performance in React applications — prefetching, service workers, bundle analysis, etc. to reduce bundle sizes.
Since its introduction 6 years ago, React has changed the way we build web apps. Several UI libraries like Vue.js and Preact.js were started after React. Existing libraries like Angular, Dojo and Ember updated their rendering libraries to optimize how DOM updates get handled, each coming up with their own way to optimize rendering performance. Many libraries and npm modules were built around React to make development easier. Today there are tens of thousands of packages around React in the NPM repository. And more are being released every day.
We are recent React converts at Rocketium. We cycled through multiple technology stacks as our company pivoted from a gaming app in 2015 to a slideshow creator in 2016 to a rich video platform since mid-2016. Our experience with React was love at first sight, and we got quick engineering wins with React. However, we soon hit a performance wall as we ported more of our product to use React. Ultimately, we had to balance new feature development and building things the right way with React. As with any tool, React is not magic and should be used thoughtfully to get the best results.
Below are some of the things we learned that should help those considering a move to React and those who are seeing their React app straining and grunting. Let us dive in!
Our journey with React
Life before React
The first version of our web app was built in 2015 with vanilla JavaScript and a patchwork of utility and UI libraries. We were a small team and manipulating the DOM or managing state in individual functions got the job done.
Two years in, we wanted to add a layer of collaboration and workflow (think Google Docs + Trello for video). Our dev team had also grown, we needed a better way to add features, ensure testability, and avoid the unpredictability of a large spaghetti codebase. This felt like the right time to dip our toes into the React ecosystem.
Moving to React
React was an easy choice since it came with a predictable flow for UI updates. Since React is not opinionated about how to manage application state or how to handle routing, we decided to go with Redux for state management and React Router for client-side routing.
Building an application with React is fun as it provides a great way to structure the UI into components. That makes it easier to visualize how the UI works. It also provides an easier way to reuse components. Redux was not fun at first but it saved us a lot of future headaches.
Hitting the performance wall
React handles UI updates efficiently but it does not magically make your web app faster. We needed to know how React works, how control flows within components, and how React updates the DOM. As our application grew in size, we started noticing some drawbacks of our setup. Although we knew how React worked and how Redux manages state, our application had bloated in size. We started seeing application crashes and jank. It was time to drive down the technical debt and make performance improvements!
Below are a few things we did and what we hope to do in the future to improve the performance of our application.
React Performance Optimization
Measuring performance with Chrome DevTools
The first step to optimising is measuring. Only once we identified the bottlenecks could we eliminate them. Chrome DevTools offers powerful performance measurements for React applications.
Chrome Devtools shows which components are rendered
Red lines show spikes in FPS
Upgrade to Webpack 4
For a year, we used Webpack 3 without much fuss. It worked really well and we had no complaints. Webpack 4 does not directly influence performance, but we upgraded to make code splitting easier. Unfortunately Webpack 4 changed many APIs making the transition a little more challenging than expected. However, this move allowed us to add a few bells and whistles to our development workflow like moving from the deprecated CommonChunksPlugin
to SplitChunksPlugin
and setting up code splitting.
Code Splitting
Our initial setup did not include code splitting by default. This was not required when the application was relatively small with few components and simple business logic. With our furious pace of product development with feature flags and A/B tests, our app added many more components and complicated logic flow. Putting all of these into one bundle was quickly bloating the application performance.
Why is this a problem? A bundle with a lot more code than is needed at all times will be larger than needed. The browser will also parse the entire application code regardless of which section of the application gets loaded. Loading extra code means slower downloads and more work for the browser, leading to slower response times. This is far more noticeable on slow connections and underperforming hardware.
To address these problems, we setup route-based code splitting using the react-loadable npm package. We then moved to React.lazy
and Suspense
despite it not supporting server-side rendering at that time. We switched because there was now a built-in way to handle dynamic imports.
Setting up code splitting with SplitChunksPlugin
was a breeze. It took a few iterations to get the number of chunks correct. Our initial bundle size for the entire applications was ~7MB. After code splitting, the initial bundles that load had a combined size of 230kb, a whopping 97% reduction in application bundle size!
Long list optimizations
There are a few places in our application where we have long lists - list of fonts, music tracks, stock images and clips, templates, motion graphics, and so on. Some of these lists made background HTTP calls and added new items to the end when we reach it, e.g. an infinite scroll. Most lists within our application did not do this and rendered a large number of items.
Rendering these items was putting a lot of strain on the browser and we saw a lot of jank. To fix this, we used an npm module called react-window which provides a higher-order component that controls the rendering of long lists. It does not render items that are not immediately visible. It supports lazy loading, custom properties, and event handlers. This gave us list rendering at 60fps.
Reducing wasted renders
State updates happen often in big and complex applications. Some of them are asynchronous, like background HTTP calls, and some happen after applying business logic. It is pretty common to see components render multiple times before any user interaction happens. It is up to the developers to detect these wasted renders and avoid reconciliation where none is required.
Wasted renders are rerenders of components that do not need to be rendered. This is especially problematic if the wasted renders happen in parent components. This is because React rerenders all the components in a tree when a parent component rerenders. This causes wastage of CPU cycles.
Finding components that waste renders
The React developer tool extension has a "Highlight Updates" option which we used to find the components causing wasted renders. After identifying the components, we fixed it in two different ways.
- Use
shouldComponentUpdate
to let React know that the component's result is not affected by the state change to control when a component should re-render. - Reorganize how components are structured and how the state changes happen. This is a more involved process and it took us quite a bit of time to identify components where rerender could be prevented.
Future steps
We have several objectives to further improve our application performance in the near future.
Use React.PureComponent
React.PureComponent
is similar to React.Component
. The major difference is PureComponent
implements shouldComponentUpdate
with a shallow comparison. This is said to improve performance but comes with a few strings attached.
Build Analysis
Visualizing builds is a great way to understand what goes into the bundles. Webpack bundle analyzer shows the separation of all the bundles generated for your application in an interactive web page. Webpack also allows us to set performance budgets so we can know when assets and bundles exceed file limits. We can also identify and remove duplicate code across bundles with third party Webpack plugins.
Better tree shaking
Tree shaking is the process of removing unused or dead code from the bundles. This is especially important when using utility libraries like Lodash where you do not need all features of the imported library.
Service Workers and PWA
Progressive Web Apps are reliable, fast, and engaging. They load quickly even in slow networks and are high-performant. They are built using the latest web technologies and deliver app-like experiences.
Prefetching
Prefetching is a way of informing the browser about the resources that are likely to be requested and fetching them before they are necessary. This is done once the required resources for the current page are loaded and when the browser is in idle state. For server-side rendered applications, HTTP/2 Push API is supported in Node.js.
Make React applications fast again
After following the steps in this article, we were able to improve our overall application performance by over 60%, by reducing the initial bundle size from 7MB to 230KB, full load time from 47 seconds to 19 seconds (slow 4G), and time to interactive from 23 seconds to 15 seconds (slow 4G), greatly improving user experience for our applications.
Improvements across the board after our optimizations
The React ecosystem is vast and powerful. We can leverage the numerous tools that are available to us to build huge complex applications. Above are just a few techniques that can make our applications fast and smooth. The additional effort is definitely worth the benefits of getting a higher-performance, more maintainable codebase.
About the Author
Ilango Rajagopal is a Senior Software Engineer at Rocketium with over 4 years of experience in building scalable web applications. He’s got an eye for great UI design and likes to discover new websites. In his free time, he likes to learn about new technologies, play games and read books.