DEV Community

Harry Dennen
Harry Dennen

Posted on • Originally published at Medium on

How 3 Lines of Code Reduced CPU and Memory Consumption by 13%

Or how assumptions can ruin the performance of your app.

We have a fairly large Angular app using PixiJS for a lot of the rendering. Due to animations and constant updates to the canvas area, we’ve had to optimize as much as possible to keep the app running reasonably well on tablets.

The standard optimizations have all been applied.

Almost all components are set to ChangeDetectionStrategy.onPush, async and broadcasts use runOutsideAngular, logging is suppressed in production, etc.

We’ve leveraged PixiJS texture cache and don’t make any .update() calls on any display objects, and the render() call on requestAnimationFrame is run outside angular.

One oversight is size of certain call stacks when major data changing events occur, but other than that things seem fairly contained.

And yet we still had users noticing performance issues.

Lots of ideas for this. Use web workers for transport, distribute chunks of functionality across separate call stacks to reduce frame drop, make absolutely all components use ChangeDetectionStrategy.onPush, detach components and reattach for updates…

A number of investigations into Angular and PixiJS performance issues were made.

Then, while investigating the memory and timing costs of angular binding, RxJS subjects, and Angular bound subjects, I created a quick Angular CLI app. After setting everything up I checked a performance recording and noticed a distinct shortage of change detection runs.

I should mention here that in order to keep our app’s frame rate decent we use requestAnimationFrame on a loop to render the PixiJS stage. What we didn’t realize was that each RAF call was triggering change detection.

But why?

Understanding How Your Tools Work is Important

Enter Zone.js. I challenge any Angular developer to explain how Zone.js works and how Angular leverages Zones.

Here’s the short version: Zone.js changes the prototypes of all async calls in the browser api (setTimeout, etc.) so that a) context can be shared across call stacks and b) that hooks can be emitted at the end of micro, macro, and event tasks.

Cool. And Angular?

Angular creates (forks) a new zone called NgZone which is a child of the root zone. This way when those end-of-task hooks are emitted, angular can run change detection on the off chance that a call has resulted in something, somewhere, changing. Which is great when you don’t have a ton of components. Not so much when you have lots.

And I’m not knocking Angular’s change detection. given the size of our app it was actually doing an impressive job running.

This issue is with the memory allocations required to run it. We ended up getting 12Mb garbage collections every 900ms.

Ok, and PixiJS?

Any event listener you register to a PixiJS object doesn’t hit the browser api. It hits PixiJS’s custom handling of events via their InteractionManager. And it’s the InteractionManager that registers a pointermove event to the document… which has been patched by Zone …which has been forked by Angular.

Ok. So our third party library is triggering change detection in our framework on mouse move — another problem — but that still doesn’t explain what’s triggering CD on every animation frame.

side note update: we resolved this using `Zone_symbolBLACK_LISTED_EVENTS`

Seriously Now, Understanding How Your Tools Work is Important

PixiJS has a nice way of handling the scenario of a DisplayObject animating past your mouse. Under normal conditions you wouldn’t get a mouseover event because the mouse didn’t move, ergo no event.

PixiJS is clever though. It’s using that pointermove event to cache the last pointer event. Then on the next tick of pixi’s event loop it will check a moving DisplayObject’s position against the cached pointer event’s location, and fire a mouseover event if the display object is under the mouse.

Ok, next tick of Pixi’s event loop though?

Turns out in order to animate all their DisplayObjects they have a shared ticker running … wait for it … as a callback in requestAnimationFrame.

So now we not only have our render loop happening 60 times a second, we also have Pixi’s ticker triggering change detection 60 times a second. Not awesome.

First prize is to get PixiJS to run in the root zone instead of Angular’s NgZone, but in the meantime these 3 lines of code stop all the unnecessary change detection runs:

const ticker = Pixi.shared.ticker; 
ticker.autoStart = false; 
Enter fullscreen mode Exit fullscreen mode

We now have 1/3 of the garbage collections as before.

Discussion (2)

johncarroll profile image
John Carroll • Edited on

Nice writeup. Not sure if you ended with a call for help, but I remember looking through the Angular Skyhook source code and it uses its own zone rather than the NgZone angular uses.

The maintainer has done a nice job commenting everything, including the custom zone setup and explaining how the interactions between the two different zones work.

May be helpful to look through: packages/core/src/connector.servic...

hdennen profile image
Harry Dennen Author

Thanks, I'll take a look.