How to miss the point when comparing web framework performance

At ng-conf 2015, I gave a talk about how we combined React and Angular at HireVue to improve our web app’s performance.

After the talk, a small number of alert internet citizens asserted that I intentionally crippled my Angular code to make it slower than my React code. They said it was not a fair comparison.

The scandal! Intentionally crippling my code?

Cripplegate. Perfgate. ngGate.

The truth is that I carefully wrote the code to represent most Angular apps I’ve seen for exactly that reason: fairness.

I believe, when comparing two web frameworks, the question is not can my app be fast with framework X? The question is will my app be fast with framework X.

Subtle, yes, but those are two very different questions.

The answer to the first question is unequivocally yes. You can build a fast web app in virtually any framework. After all, you can do this in Angular:

app.controller("MyCtrl", function($element) {
$element.innerHTML = '<div>Giant HTML here</div>';
});

Boom! High performance Angular!

But the answer to the second question matters a lot more, and it’s much more nuanced.

Because every web framework is a mixed bag of performance tradeoffs.

Also, there are a bazillion reasons to choose a web framework besides performance, but this article is only about performance.

So let’s talk about some of the features I did and did not use in my ng-conf talk.

Angular 1.x Performance Features

Angular has several performance-related features, each with their own tradeoffs:

  • $timeout’s invokeApply parameter allows you to bypass the digest and manage it yourself, which can be faster.
    Tradeoff: You have to tell Angular when to digest. If you make a mistake, your UI won’t update until the next digest. Also, this is not documented as a performance improvement feature.
  • $http’s useApplyAsync parameter batches digests from multiple HTTP responses that arrive within 10ms of each other, resulting in fewer digests and thus better performance.
    Tradeoff: Performance isn’t deterministic because it depends on HTTP response times. Also, not available in Angular 1.2 (hence, no IE8 support).
  • The Scope $digest method allows you to only digest a single scope’s sub-tree, which can be less costly than digesting the entire $rootScope.
    Tradeoff: If you don’t $digest the right scopes, your UI won’t update until the next digest, and this can make very hard-to-find bugs.
  • ng-repeat’s “track by” minimizes DOM re-renders.
    Tradeoff: This one is a no-brainer. Just use it. However, Angular won’t warn you if you forget. It will just silently run slow.

From an admittedly subjective reading of the docs, only one of these features was obvious enough to me to be used in my talk: “track by” (hint: it made no difference in the performance of my code).

Which was exactly my point: out of the box, you’ll have to do two things to get these performance benefits:

  1. Find out about them
  2. Change your code to use them

In my talk, if I had used $timeout’s invokeApply feature, combined with local $scope.$digest(), yes, the Angular 1.x code would have run pretty fast. But the point was to simulate lots of $http requests in flight, and from what I can tell, $http provides no invokeApply feature.

Also, if you read all the way to the end, you’ll find that there’s more!

Of course, I could use Jeff Cross’s suggestion (this is a joke for humor purposes only):

Note: Correction is mine.

React Performance Features

React also ships with several performance-related features.

  • The key prop. This behaves almost exactly like Angular’s “track by” feature. And, if you forget to include a key prop on a list of child components, you will see a warning in the console. React is nudging you toward better performance, even though you didn’t read the docs.
  • The render callback. When rendering a React component, you can pass a callback function that will be called after the component is finished rendering. If something slow is happening, you can find out by measuring render times in this callback.
    Tradeoff: It’s a pain to add callbacks all over the place, and you normally don’t call render() directly in React apps.
  • React ships with performance tools. This gives you a nice break down of how long each component spends rendering, and how many instances of that component are currently in the DOM:

Including how much time was wasted by reconciling components that didn’t need to re-render to the DOM:

  • React batch updates. By default, React batches DOM updates to maximize performance. You do nothing and get this for free. As a result, initial renders are very fast, and subsequent DOM mutations are too.

Nudges

When I choose a framework, I’m choosing a friend that will influence my behavior. Like peer pressure. Like that time you said no to drugs in high school because you didn’t want to disappoint your friend.

I choose my friends carefully. Oh who am I kidding — everyone’s my friend! But if I actually chose friends carefully, this is what I would ask:

  • Will this framework nudge me into building a high performance app by providing high performance defaults?
  • Or will I have to go out of my way to get performance?
  • Will I only find out about performance features after my app starts running slowly, and I go looking for them?
  • Does the framework provide tools for measuring performance?
  • Or will I have to install third party stuff to measure performance?
  • If I make a performance mistake, will the framework warn me?

Getting to the point

So if you really want to miss the point when comparing two web frameworks, be sure to focus on what is technically possible with them, and ignore the behavioral nudges that the frameworks give you.

Every framework will nudge you and your team. The only question is where will the nudges send you?

Nitpicker’s Corner

Oh, one more thing. No one seems to have mentioned this, but there were two parts to the performance demo app I built for the ng-conf talk:

  1. Initial load (clicking the “Load” button)
  2. Updates after load (clicking the “Search all month” button)

So far, all the discussions I’ve seen are about #2. And indeed, that’s where invokeApply:false, and local $scope.$digest() could have helped (hopefully, I made it clear above why I chose not to use them).

But what about #1?

For the initial load scenario, React was about 5–6 times faster than Angular 1.x. This is because React is able to batch all the DOM operations for the initial load of the table.

React:

Angular 1.x, on the other hand, cannot do this, because of its design with directives and such. The same operation on Angular 1.x takes 5 times longer:

Angular 1.x:

So far, no one has been able to make this faster in Angular 1.x. If you can, let me know.

To be fair, Angular 2 will be just as fast (if not faster) than React for this particular example, based on the measurements I made a few months ago Of course, Angular 2 moves fast, so they may not be valid any more, and I didn’t post them here for that reason.

Conclusion

There is no conclusion. That’s the whole point of missing the point. Have a nice performant day.