JavaScript and Immutability: Can You React Fast Enough?
React is very fast, but in order to keep a nice pace, it needs help in understanding which parts of the view should or shouldn’t be updated. This problem of which view components need to be updated given some changes in the model can be solved using different strategies.
React suggests a particular approach: using persistent data structures.
Immutability makes tracking changes cheap; a change will always result in a new object so we only need to check if the reference to the object has changed.
Enter Immutable-JS
React recommends the use of a library also developed by Facebook, immutable-js. It draws inspiration from implementations of persistent data structures that have already been production tested in functional programming languages, such as Scala or Haskell.
These implementations are based on a data structure called HAT-trie which allows structural sharing and minimizing the need to copy data.
Caution: Speed Bumps Ahead!
Initially we were happy with the performance of the React and immutable-js duo. However, we soon come across a use case that made obvious the lack of performance when changing the model: doing a big number of small changes over very small records.
Records have a fixed, known set of attributes. The number of attributes in a record is generally small. From our experience, more than 90% of record class definitions have less than 20 attributes, and more than 90% of record usages have under 35 attributes.
In immutable-js, the implementation of records is currently based on the implementation of Maps, with the extra bit being restrictions on the fields you’re able to use. This implementation is good for an arbitrary-sized key-value collection like a regular map usually is. But for fixed-attribute records, usually with a small amount of attributes, does the added complexity of HAT-tries outperforms copying all data?
Copying all data may sound silly. It sounds like giving up the advantage of structural sharing: copying less data, which means using less memory and wasting less time copying. But for a small attribute count, the memory needed to copy all attributes will be comparable to the memory used by the new trie structures. Also, the copy process will likely be more efficient as it won’t require navigating the trie structure.
A persistent structure that requires a full copy for a mutated version isn’t a new concept, and it’s also present in a lot of functional programming languages, taking names such as Record or Named Tuple.
The Quest for a Better Record
After a few tuning iterations that will be detailed a future article, we created a factory function that, provided a set of default values, dynamically creates a constructor of Record objects with get and set methods. This is pretty much like immutable-js, which allowed us to use it as a drop-in replacement for Immutable.Record in our code.
The code of our implementation is available at GitHub.
Using our version of Records yielded good performance improvements in the use case that sent us off on this quest. After verifying the gains in the real-world scenario, we were still curious on how many attributes a record would need for the added complexity of the HAT-trie to beat the full copy. So we built a little benchmark and ran it in node.js.
time% attributes
--------------------------
5.5% 5
6.6% 10
8.8% 15
15.3% 20
20.9% 25
22.9% 30
23.7% 35
25.9% 40
32% 45
32.7% 50
36.8% 60
43.4% 70
49.3% 80
66.1% 90
66.9% 100
75.8% 120
225.5% 140
In this test case, for set operations on records under 50 attributes, our implementation ran about 3x faster than Facebook’s. In records under 20 attributes, it runs in 15% of the time — that’s about 6x faster than the original implementation! Get operations were consistently faster despite the attribute count of the record. Adding them to the benchmark would always bias the numbers in favor of our implementation. Immutable-js gets the lead again once the attribute number is high enough to make V8 give up optimizing the object that we use to carry data.
Deliver Fast
It makes a lot of sense to stand in the shoulders of giants, and using React and immutable-js is a good choice. We still think that Immutable-js is a great library for most use-cases.
However, it doesn’t hurt to know your use cases well and how they fit the algorithms and data structures that you’ll be using — sometimes there might be better alternatives.
Performance analysis and tuning of JavaScript applications and libraries is a lot of hard work, but it did allow us to deliver much faster applications.
And it left us with a few stories for future posts.
Further Reading:
The ideas shared in this post resulted partially from the research project RADicalize, which was supported by the EU and PT2020.