In my last change detection post, we looked at an Angular demo where the central component rendered its own component tree as a literal tree graph, with nodes and edges and whatnot. That demo allowed us to visually follow the change detection system’s path through an actual component tree as it searched for and detected changes.
In this post, we’ll pick up where we left off and use a couple of small Angular apps to look at some high-level ideas undergirding the OnPush change detection strategy. You’ll learn how the change detection system traverses a tree of components using the OnPush change detection system. You’ll also see how the Observables-plus-async-pipe duo helps us manage the task of dirtying out-of-date components.
Searching the component tree
You may have noticed in the last blog post that once change detection is triggered, the default behavior is to do a depth-first search through the entire component tree, searching out changes. The appeal of setting our component’s change detection strategy to OnPush is performance—it narrows the change detection system’s search space to branches of the component tree that are more likely to be out of date.
We can see this behavior in action in the demo below. Again, we have a
component rendering its own component tree as a literal tree graph. (If you browse the component templates and start inspecting the DOM with your dev tools, you will find the graph reflected in the DOM’s structure.) All of the components use
Clicking a graph node marks the component rendering that node as out-of-date or, rather, “dirty” and a change detection cycle ensues. (We will discuss this and other change detection triggers for OnPush components in the next section.) Go ahead and click a few of the component nodes; when you do, you will see that the component tree highlights certain nodes with a flash of green. These flashes indicate that the node was visited by the change detection system and checked for changes.
Hopefully, this illustration underscores how much more efficient the change detection system is when searching through a tree of OnPush components. It still follows a depth-first search—same as the default change detection search behavior—but sticks to branches of the tree that contain components marked as dirty. It also stops once it reaches the deepest dirty node.
Marking components dirty
There is a trade-off, however, to the performance gains OnPush components give
us. When we use them, the framework makes the component developer partially responsible for letting it know about the out-of-date components by marking them dirty. This helps the framework know when it’s time to run change detection and also which set of branches in the component tree it needs to search.
There are three ways of dirtying a component. A component is marked dirty when:
- A DOM event bound in the component’s template is fired. (The demo in the last section relies solely on this method.)
- The component’s parent changes one of the component’s inputs.
- The component calls
Dirty triggers 1 and 2 happen implicitly; the framework does the dirty marking for us. Trigger 3, however, is where we step in. When components become dirty for reasons outside of the scenarios described in 1 and 2, then
markForCheck must be called. Calling
markForCheck can be a burden because we often have to do it manually. (The official documentation has a simple example of manually calling
markForCheck in a component that relies on an internal timer.) Angular does, however, try to mitigate some of this responsibility.
Observables and the async pipe
One of the most common scenarios in which a
markForCheck is required is when
a set of components renders data from a shared injectable service. When one
component updates the service’s data, it puts the other components out-of-date. If they don’t get marked dirty, then after change detection finishes, their views still won’t reflect updates to the data. (Components failing to re-render is a common bug introduced by using OnPush components.)
One of Angular’s preferred solutions in this situation is using
async pipe. This solution has two parts: First, we provide access to the
service’s shared data via getters that return
Observables. Then in our
templates, we pipe these
Observables through the
async pipe. The async pipe
“observes” changes to the data and outputs the new values for rendering.1
async pipe also calls
markForCheck whenever there are
changes, putting the template’s component on the change detection system’s
In hello-world terms, this may look something like this:
Let’s see this pattern in action:
The example is a little contrived, but here we have two views that share a common injected service. The service,
Matrix2x2Service, represents a two-by-two matrix. The views each represent two ways of editing the matrix’s entries. On the left, we have the
Matrix2x2EntryEditorComponent, which allows for editing matrix entries with simple inputs, and on the right, we have the
Matrix2x2DragEditorComponent, which allows for editing the matrix entries by dragging around the matrix’s column vectors.
Looking at the
Matrix2x2Service service, we provide the
getEntryObservable getter method, which returns an
Observable that emits the most up-to-date value of an indexed matrix entry:
Then, in the
Matrix2x2EntryEditorComponent's template, we render those matrix
entries using the entry
Observables and the
Matrix2x2DragEditorComponent similarly listens to the
Observables returned by the
Matrix2x2Service and uses the
async pipe to update its view.
Notice how as we interact with one component, the other component's view updates
accordingly. The duo of
Observables and the
async pipe turn the manual task of
markForCheck into one that happens implicitly, just like the other
A few miscellaneous benefits
As an aside, it’s worth pointing out a couple of other nice benefits that
Observables and the
async pipe offer as a pairing and individually:
asyncpipe handles the job of managing
Observablesubscriptions. When a component is destroyed, the
asyncpipe automatically unsubscribes.
Observable's wealth of "operators" (e.g.,
filter, and other operators familiar to functional programming) lets us take a declarative approach to transforming updates on our data.
- For Angular apps built atop a code base that uses alternative event libraries (e.g., Google closure's
rxjs, provides an adapter API for turning them into
By making our components use the OnPush change detection strategy, we have been able to squeeze some additional performance out of our Angular apps. Hopefully this post has clarified some of the high-level ideas behind this practice and has given you some code to play with. For deeper dives into some of these areas, please see Ben Dilts’ post on OnPush performance and Sriraam Subramanian’s post on
Observables in multi-view apps.
1 I like to think of think of this in terms of "wrapping" and "unwrapping": We wrap the shared data in an
Observable and then unwrap it with the
I have been using your LucidChart for some years, and is an avid fan. I was thrilled to know that you use Angular for the UI. Your UI is very complex, so may require a state management on the client side. Do you use redux or NgRx or keep the state in the server so every state change is sent via an API to a DB in the server?
Appreciate your feedback.