Measuring performance gains — AngularJS to React (with Redux or Mobx)

If you’re looking into migrating a large AngularJS single page application (SPA) to React and wondering what sort of performance gains you are going to get with React and how the code will morph (with state management libraries Redux or Mobx), this post will try to answer some of these questions.

In this post, I will go over the performance and memory profiles of a various UI scenarios implemented using AngularJS, React/Redux and React/Mobx. We will compare and contrast the performance of these frameworks on measures like script execution time, frames per sec and usedJSHeapSize for each scenario. I provided the links to the test pages and source code so you can try out those scenarios and can review the code to get a feel for constructs that React (with Redux or Mobx) will bring to the table.

Performance test setup

To evaluate the performance of AngularJS and React, I created a benchmark application, a stock ticker dashboard. This application shows a list of stocks and has some controls to automate UI test actions. For each stock, the application shows the ticker symbol, company name, sector name, current price, volume and simple moving averages (10 days, 50 days and 200 days) and a visual indicator showing whether the price went up or down. The test dataset consists of 5ooo stock tickers and is loaded during the page load via script tag.

I created three versions of this application using AngularJS, React/Redux and React/Mobx. This enables us to easily compare the performance metrics for each scenario across the frameworks.

Performance test page

Test Scenarios

  • Switching views
    We navigate through a list of 5000 stock tickers showing 150 tickers at a time every 0.5sec. This scenario measures how quickly the framework can update the view when the visible collection data model changes. 
    Real world use-case: route changes, paging through a listview, virtual scroll etc.
  • Adding tickers
    We add 50 tickers to the visible collection every 100ms until we show the entire collection of 5000 tickers. This scenario measures how quickly the framework can create new items. Showing 5000 tickers is not a realistic scenario but we can visualize the limits where things will fall apart with each framework.
    Real world use-case: Pinterest style infinite scroll where new UI elements are added to the DOM as the user scrolls.
  • Quick Updates to Price/Volume
    We render 1500 tickers and start updating price/volume data for random tickers once every 10ms. This scenario measures how quickly frameworks can apply the partial updates to the UI. 
    Real world use-case: updates to presence indicators, likes, retweets, claps, stock prices etc.
  • Removing tickers
    We will first add all 5000 tickers and then start removing 50 tickers from the visible collection once every 100ms.

Links to test pages and source

All the examples are written in Typescript and the compilation/bundling is done using Webpack. The Readme page in source url, lists the instructions to build and run the applications.

Before we start…

  • All the below metrics are measured on Win10/Intel Xeon E5 @ 2.4GHz, 6 core, 32GB desktop with Chrome browser v60. The numbers will change on different machines/browsers etc.
  • To see the accurate heap memory data on the test pages, open Chrome with ‘--enable-precise-memory-info’ flag.
  • React is a library rather than a full fledged framework like AngularJS. In this post, I used the term framework for simplicity.
  • Test pages show live JavaScript heap size as Memory.
    About javascript heap size - From Fix Memory Issues by Kayce Basques - In Chrome TaskManager, “The Memory column represents native memory. DOM nodes are stored in native memory. If this value is increasing, DOM nodes are getting created. The JavaScript Memory column represents the JS heap. This column contains two values. The value you’re interested in is the live number (the number in parentheses). The live number represents how much memory the reachable objects on your page are using. If this number is increasing, either new objects are being created, or the existing objects are growing”.
  • About Frames per second - From Rendering Performance by Paul Lewis — “Most devices today refresh their screens 60 times a second. If there’s an animation or transition running, or the user is scrolling the pages, the browser needs to match the device’s refresh rate and put up 1 new picture, or frame, for each of those screen refreshes. Each of those frames has a budget of just over 16ms (1 second / 60 = 16.66ms). In reality, however, the browser has housekeeping work to do, so all of your work needs to be completed inside 10ms. When you fail to meet this budget the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience.

DOM - AngularJS Components vs React Components

AngularJS directives (or components) create an extra wrapper element around the template. For simple views, this is not an issue. However, in complex views containing a large number of directives (esp. when they are repeated within ng-repeat), all the extra elements will add up to the total size of the DOM tree, — potentially impacting memory, selector performance etc. Although, you can set ‘replace=true’ property to disable rendering the wrapper element but it causes a bunch of issues and is currently marked as deprecated.

Here is the rendered html for the ticker component in AngularJS:

AngularJS directive/component (left side), Rendered html (right side) - Wrapper element is created for each child directive.

Here is rendered html for the similar ticker component in React:

React component (left side), Rendered html (right side) - No wrapper elements created for child components

In our specific example, AngularJS created an additional 1400 DOM nodes compared to React for rendering the same number of tickers (200).

DOM count — AngularJS vs React

Scenario 1 — Switching Views

We navigate through a list of 5ooo tickers showing 150 tickers at a time every 0.5sec.

Below chart plots the script execution time for each refresh from Chrome’s performance timeline. AngularJS consistently took >200ms to delete the existing 150 tickers and to show the new ones. Where as React/Redux did the same work within 90–100ms (half the time compared to ng1). React/Mobx version took slightly little more time than Redux version but not far from it.

Script execution time comparison (AngularJS vs React/Redux vs React/Mobx) — Replacing 150 tickers every 0.5 sec

Below chart shows the frames per sec(fps) as the refresh happens. Redux and Mobx versions stayed around 45fps whereas AngularJS stayed around 30 fps during the entire run.

Memory & GC pauses

Below chart shows the javascript heap size (‘usedJSHeapSize’) measured during the refresh. Both AngularJS and Mobx versions showed staircase pattern for the memory consumption, indicating that Chrome kicked in the GC to reclaim the memory. Redux version is super consistent with its low memory profile all throughout the run.

Let’s closely look into the timeline profiles for all the three versions.

AngularJS execution caused several GC pauses as the ticker list gets refreshed. V8 tries to hide GC pauses by scheduling them during the unused chunks of idle times to improve the UI responsiveness. Contrary to this ideal behavior, GC pauses happened during the script execution contributing to the overall execution time

AngularJS emitted lot of GC events as the ticker list is refreshed with 150 new items

Redux performance profile shows no GC pauses whatsoever during the script execution.

React/Redux — No GC pauses

Mobx profile shows few GC pauses but not as many as AngularJS version.

React/Mobx — Few GC pauses but not as many as AngularJS version

Scenario 2 — Adding Tickers

We will add 50 tickers to the visible collection every 100ms until we show all the tickers. The end result of showing all 5000 tickers is not a realistic scenario but it would be interesting to see how each framework handles it.

Below chart plots the script execution time from chrome’s performance timeline. In the case of AngularJS, the script execution time linearly increased as more and more tickers are added to the page. AngularJS took more time to add new tickers right from the start compared to the other versions. Interestingly, Redux and Mobx versions show impressive performance even towards the right side of the chart with thousands of tickers on the page. React’s virtual dom diffing algorithm is showing its strength compared to AngularJS’s dirty checking.

Adding Tickers — Script execution time comparison

With AngularJS, adding new items caused jank in the browser right from the start (red bars) and the number of frames per second dropped from 60 early on and never recovered (green area) during the entire add operation.

AngularJS — Add tickers timeline

Redux created jank once early-on but it is all clear until we crossed half the way of adding new tickers. FPS also nicely recovered to 60 in between the add operations.

Redux-Add Tickers timeline

Mobx caused jank few times more times than Redux but nowhere close to AngularJS.

Mobx — Add tickers timeline

Memory & GC events

Redux consumed about half the heap size as AngularJS during the entire run. Mobx stayed in between.

Adding Tickers — Memory Comparison

Adding new tickers also triggered quite a number of GC pauses with AngularJS (almost once with every add operation). Redux triggered less GC pauses overall. Mobx started to trigger more GC pauses towards the end as we add more and more tickers to the list.

Adding Tickers — AngularJS GC events (partial timeline)
Adding Tickers — React/Redux GC events (partial timeline)
Adding Tickers — Mobx GC events (partial timeline)

Scenario 3 — Quick Updates to Price/Volume

This is the most common scenario in the real-time applications. Once the view is rendered, there is will be a quick succession of updates coming into the application either via web-sockets, xhr calls etc. Imagine the use-cases like presence updates, stock price changes, likes/retweets/claps count changes etc. Let’s see how each framework fares in this scenario.

All the below metrics are taken with 1500 tickers on the page and price/volume changes are happening every 10ms.

AngularJS again struggled to keep up with the updates happening in quick succession. Script execution for each update took about 35ms. Redux took 6ms to update the view. Mobx really shines updating the view within 2ms. Mobx’s derivation graph knows exactly which component to update based on which observable’s state is changed.

Updates — Script execution comparison

Here are the timeline profiles showing the script execution for each version.

AngularJS — Updates to Price/Volume
Redux — Updates to Price/Volume
Mobx — Updates to price/volume

FPS consistently stayed at 60 with Redux and Mobx where as it hovered little below 30 with AngularJS.

Updates to Price/Volume — Frames Per Second

Scenario 4 — Deleting Tickers

We will add all 5ooo tickers to the page and start removing 50 tickers from the visible collection every 100ms.

Below images show the performance profile of the initial delete iterations. AngularJS is almost 4x slower compared to React versions. Redux and Mobx took little more time in the initial iterations but settled between 50–70ms for each delete operation.

AngularJS — Deleting 50 tickers from 5000 tickers every 100ms (initial iterations)
Redux — Deleting 50 tickers from 5000 tickers every 100ms (initial iterations)
Mobx — Deleting 50 tickers from 5ooo tickers every 100ms (initial iterations)

It is quite obvious from all the above tests that React gives significant performance gains when compared with AngularJS.

As the applications grow bigger and views get complex, runtime profile of the frameworks starts to differ in their own way. Our objective is to replicate the scenarios we are targeting for, measure the performance/memory impact and look at the pro/cons of the constructs with each framework. Even with the most performant framework out there, we still need to apply a lot of discipline and follow the right patterns to make the applications scalable and performant.

I will go over the core concepts, benefits and gotchas of Redux and Mobx in a separate post. (Update: it is live now).

Thanks for reading. Hope this is helpful.

P.S. Thanks to Shyam Arjarapu and Adam Carr for reviewing this article.