Immutability Refactoring FAQ - adobe-photoshop/spaces-design GitHub Wiki

Why immutability?

In a word: performance. Two factors dominate our application performance: 1) waiting for Photoshop; and 2) rendering the view. Besides making Photoshop faster, our strategy for 1) is to emulate Photoshop piece by piece in JavaScript so that we can update our models without waiting for Photoshop at all. Our strategy for 2) is to leverage React's two-phase rendering algorithm, in which a first pass renders to a virtual DOM, and then a second pass diffs the virtual DOM against the browser DOM, resulting in approximately minimal updates to the browser DOM.

But the virtual DOM is not a performance panacea. Creating the virtual DOM can be expensive, especially as it grows. (We need to cope with large virtual DOMs because we show a lot of information directly about large PSDs.) In particular, the first pass of React's two-phase rendering algorithm involves by default, calling the render function on any part of the virtual DOM subtree for which state has changed, which means calling renderon all children of changed components, instantiating and mounting new child components, and unmounting dangling child components. This can take a long time!

The time spent rendering (and re-rendering) the virtual DOM when no actual browser DOM updates occur is called "wasted" time in React terminology. In one test with a moderately (but not insanely) large PSD, it takes ~2500 seconds to start up, of which ~500ms is wasted. And this repeats every time the view is re-rendered for that document; e.g., after switching to another document and back. This is a huge amount of wasted time, which contributes to a feeling of sluggishness.

React provides a solution to this problem though: a method, shouldComponentUpdate, which can be implemented by each component. This method may compare old and new state and props and determine whether the render method should be called at all. And, of course, when it returns false, the render method of child components is not called either.

Implementing shouldComponentUpdate well can be difficult, though. We want a test that is 1) fast, because it is called after each state change; and 2) sound, meaning that if some state has changed that necessitates a browser DOM update then it must return true.

The trivial implementation of shouldComponentUpdate, which is both fast and sound, is simply to return true, which is the default behavior of React components. Another sound implementation would be to perform a deep structural equality test, but that would fail the "fast" requirement, especially with large models. On the other hand, reference equality, while certainly fast, fails the soundness requirement: let oldState = [] and newState = oldState ; newState.push(42), but then it is still the case that oldState === newState.

One possible solution is to leverage the speed of reference equality, but in a sound way. Note that the problem with the previous solution lies with the mutability of JavaScript array objects. If, instead of mutating the original array, we had created a new one, the test would have been both fast and correct: i.e., if oldState = [] and newState = oldState.concat(42) then reference equality behaves as desired since it is no longer the case that that oldState === newState. If we are careful to ensure that we never mutate state then we can bend reference equality to meet our needs. This is a tall order, though. JavaScript is a naturally imperative language, and mutation as a side effect is easy to stumble into. To help us stay on the immutable path, we'll need help.

How can we do it?

To help ourselves make correct use of immutable state (in particular, immutable models), we'll use a third-party library for building and updating immutable data structures called, unsurprisingly, Immutable. Immutable isn't tied to React, but it is philosophically aligned and also built by Facebook. I've found it to be coherent and enjoyable to work with. With Immutable, the previous example is worked as follows:

oldState = Immutable.List(); // Empty list constructor
newState = oldState.push(42); // creates a new list; has no effect on oldState

The Immutable API is generally similar to that of built-in JavaScript data structures like Array and Map, but none of of the operations have side effects. (There are a lot of interesting details to Immutable, like lazy sequences, that I won't get into here.) But note that, generally, the API is not strictly compatible either. This is important enough to re-iterate: using Immutable means giving up on using Array and Object and Map as data structures because they are uncontrollably mutable. Consequently, the switch to immutability and Immutable in particular is a rather drastic change; we use a lot of objects and arrays!

Will this create a lot of garbage?

A possible concern with using immutable data structures is that it could lead to significantly increased memory garbage. I can't say with certainty that this will never be a problem, but Immutable has a few features to mitigate the problem. First, it makes it possible to perform deep updates to data structures with out deep copying. Which is to say it performs as little copying as possible, and instead makes careful use of structure sharing. So, flipping the "selected" flag in a layer does not cause the entire document model to be copied. Second, there is support for temporarily converting an immutable model to a mutable one so that a sequence of mutations can be performed efficiently, creating just one extra model in memory instead of many.

(And, of course, if the goal is to have a function which transforms one immutable data structure to another, there is no requirement that the implementation of that function not use mutable intermediate data structures. The Immutable library goes to great lengths to limit the need to resort to this, but it's always an option.)

What's the status?

The good news is that most of the biggest changes have already been made in a branch. An outline of the major changes is as follows:

  1. All of the models in js/models are now classes that inherit from the Immutable.Map data structure, making them immutable. Their mutator methods now return modified clones. Models all have as single constructor which takes a single object parameter that initializes the model's values. Model constructors have static methods which instantiate some number of models, typically by parsing Photoshop descriptors.
  2. There is now only a single DocumentStore for managing Photoshop document models, which subsumes the functionality of the previous LayerStore, BoundsStore, StrokeStore, FillStore, LayerEffectStore and RadiiStore. The functionality in those stores seemed to be mostly boilerplate, but what wasn't either comes for free after the refactoring or is better suited to their respective models. The Document model is responsible for directly instantiating and updating its constituent models, e.g., Layers, Bounds, Strokes, etc.
  3. The API of the "layer tree" model has changed significantly. For example, we now access the layers data structure as document.layers, and the sub-list of selected layers as document.layers.selected. In some parts this was "optional" work to improve uniformity and coherence. In other parts this was due to limitations of immutable data structure, e.g., their inability to directly represent circular references. In particular, layer relations like child or parent are now maintained separately from the layer models themselves.
  4. There is basic support for lazily computing and caching information derived from the immutable models. We now make some effort in particular to cache and lazily compute information derived from the layer models; in particular their various interesting subsets for superselect and outline visualization.
  5. A few components have shouldComponentUpdate implemented, namely Properties and Layer. The former improves startup time and the latter improves selection-change time. As you'll see, having immutable models is not a panacea, but it gives us the tools we need to address one particular aspect of application performance. There is more work to be done here.

What's the catch?

The immutable branch works well. In many ways it is faster than the mutable master branch, and in many ways it is much simpler. (A lot of code has been removed!) The catch, as previously mentioned, is that we will have to continually be aware of the distinction between mutable and immutable data structures, which adds complexity. In other words, we'll always have to decide whether to use, e.g., a mutable Array or an Immutable.List in our implementations and our APIs. Mixing the two does not generally work well. I've ported 90% of the relevant code to use Immutable data structures (and all of the code that required porting), but there's still a bit of mess left over. And third-party libraries will always require us to convert back to standard mutable data structures at some point. For example, React child components must currently be specified as an Array of components. This isn't hard, but it is another thing to keep track of.

What's next?

The immutability branch contains changes to almost every file in the repository. In many cases, porting from mutable to immutable data structures required significant changes. Notably, almost all Lodash code was replaced by similar but not identical Immutable code. Hence, although I'm not currently aware of any bugs or missing functionality, the opportunity for breakage is quite high. I need your help to test my changes and to check my work. Please look over the parts of the code that you personally wrote, and that I've trampled upon with my changes. In most cases I tried not to change the essence of what I found, but I also haven't been shy about cleaning things up that I thought needed it. Some of my changes were more superficial than others: I made deeper changes to the Document sub-models (stroke, fill, dropshadow, etc.) than e.g. to the Scrim overlays.

Please work with me to ensure that I haven't screwed anything up!