"Seeing" Angular Change Detection in Action Part II: OnPush, Observables, and the Async Pipe
Ty Lewis
Reading time: about 8 min
Topics:
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 ChangeDetectionStrategy.OnPush
.
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
ChangeDetectorRef.markForCheck
.
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 Observable
s and the async
pipe. This solution has two parts: First, we provide access to the service's shared data via getters that return Observable
s. Then in our templates, we pipe these Observable
s through the async
pipe. The async pipe "observes" changes to the data and outputs the new values for rendering.1 Internally, the async
pipe also calls markForCheck
whenever there are changes, putting the template's component on the change detection system's search path.
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 Observable
s and the async
pipe:
The Matrix2x2DragEditorComponent
similarly listens to the Observable
s 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 Observable
s and the async
pipe turn the manual task of calling markForCheck
into one that happens implicitly, just like the other dirty triggers.
A few miscellaneous benefits
As an aside, it’s worth pointing out a couple of other nice benefits that Observable
s and the async
pipe offer as a pairing and individually:
- The
async
pipe handles the job of managingObservable
subscriptions. When a component is destroyed, theasync
pipe automatically unsubscribes. Observable
's wealth of "operators" (e.g.,map
,zip
,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
goog.events.EventTarget
system),Observable
's library,rxjs
, provides an adapter API for turning them intoObservable
s.
Conclusion
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 Observable
s in multi-view apps.
Footnotes
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 async
pipe.
About Lucid
Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.