Speeding up your (js) React app using Chrome’s Performance Monitor
What I’d like to talk about here is Google Chrome’s real time monitoring tool — Performance Monitor, it’s metrics and how to make sense of them. Even though it’s been with us for years now, I’ve heard little talk about this useful console gadget, and that may be because of the in depth knowledge of javascript required to reach conclusions based on what we receive from this tool in terms of metrics.
To keep it short, Performance Monitor gives us a monitoring interface through which we can easily see real time data for
- CPU Usage,
- our current heap size,
- the size of our DOM,
- the number of event listeners,
- documents,
- document frames, and
- the number of layouts per seconds and
- style recalculations per second.
But what do these metrics tell us exactly? Data is useless without context, so I will try to go over these metrics below, hopefully shedding some light on this little known, colorful interface. I’m mostly a React developer so I will use examples from the React world where applicable.
CPU Usage
A javascript app shouldn’t be extremely taxing on your CPU. Your app is most likely to be found on the internet and accessible by handheld devices, so if your usage percentage shoots up during normal app use, you’re probably doing a poor job optimizing your code.
You might be suffering from a memory leak, or you may be trying to animate too many things at once — if you aren’t trying for a spot on the awwwards.com you could be overdoing it.
From the more concrete, React aspect, make sure you’ve virtualized your long lists of data and look for spots that could benefit from useMemo
or useCallback
hooks. Sometimes you’re triggering layouts and style recalculations (which cost CPU ticks) with unnecessary rerenders.
Regarding the useMemo
and useCallback
hooks — make sure you pick spots where there IS a performance benefit, rather then use them all the time because you could be adding unnecessary complexity to your code and ultimately achieving the opposite effect of a performance boost).
JS Heap size
If you look up the computer science definition of the “heap”, you will see that it’s a priority queue which is used for memory management of your app during runtime.
Basically, you allocate some memory for your object, you use the allocated memory (read/write), and (in higher level programming languages) a garbage collector releases that memory address for further use. If you find this complicated, I have great news for you. Garbage collection in javascript is completely automated, and as of 2019, it is not possible to explicitly trigger garbage collection in your js app.
So why are we even talking about this metric?
Well, as good as the automated garbage collection algorithm of our v8 engine is, it won’t protect us from memory leaks. If you find your RAIL model to be flaky or failing, rev up the Performance Monitor and figure out what interactions cause the heap to start building up. A large heap, and high CPU usage are bound to cause visibly bad user experience.
Not a lot can be directly done here (especially if you’ve gone through some optimizations which were already choking the CPU), BUT some good practices can go a long way, especially on long running projects. Become aware of the life cycle of your variables and functions. Find out what destructuring arrays and objects does, learn how to deep copy an object in order to avoid referencing it (hint: this allows it to be garbage collected from the heap), and find out what scopes and closures are. Oh, and definitely get a good grasp of asynchronous activities in your app. Even though you can code your way through most projects without knowing too much about said things, a lack of attention to details adds up when you get to enterprise level. That being said, doing these kinds of micro optimizations probably won’t be as effective as finding alternatives to high level strategies you’ve taken in your app (how and when you load your data, how many DOM nodes are necessary for your user experience, which packages you’re using and whether they can be replaced or removed from the flow… etc).
If you want to go beyond performance monitor, creating heap snapshots is a good way to analyze an out of control heap.
Important note here — a large heap isn’t necessarily a problem, the heap size is monitored for continual increase. A heap that grows but shrinks afterwards is a “healthy” heap being garbage collected. A steady growth pattern of a heap indicates a memory leak that is to be dealt with.
DOM Nodes
An excessive DOM tree size will hit you hard, most commonly in the network efficiency and load performance department, followed by runtime and memory performance.
If you’re using React, make sure you use a windowing library like react-window to virtualize your long lists (we’ve spoken about this) or look into loading your data sequentially (pagination or infinite scrolling) — you don’t need all of the DOM, all of the time. Your DOM needs proactive crowd control.
Make use of code splitting and/or consider lazy loading parts of your DOM.
JS Event listeners
Event listeners are a way of registering a handler function to an interaction with a DOM element. Apart from a few favorites like click, hover and keyup, you’d be surprised how many event listeners are available.
What you want is to always have a minimum of event listeners present on your page, and apart from reducing your DOM size, if you’re dealing with React, you might want to make use of the useEffect
hook’s cleanup function. Use an empty array as a dependency array for your useEffect
hook to add an event listener on component mount and always pair your addEventListener
, with a removeEventListener
in a cleanup function. The cleanup function is run when a component is dismounted AND before each re-render (yes, this is a good thing).
Speaking of event listeners, adding an asynchronous task to an event listener of a DOM node which can be part of an unmounted component during your app life cycle is a sure fire way to cause a memory leak…
Ultimately, look into reducing the number of DOM nodes on your screen as event listeners are always linked to elements.
Documents
The number of document type resources and iframes requested and rendered AND still held in memory (not garbage collected). You might want to look into this if your heap is large. If you find the number of documents “odd” you’re best of looking at the DevTools Timeline/Performance panel to identify resources you may be requesting ahead of time, or keeping after use.
You can also use DevTools to download the .har document of your page and search for “_resourceType”: “document” in there to name and shame in great detail. These .har (http archive) files are detailed accounts of the browser’s interaction with the site and they are generally used for identifying performance issues, such as bottlenecks and slow load times, and page rendering problems.
Document frames
The number of document frames in your page still held in memory (not garbage collected). Look through your Sources panel in DevTools or look at the top left of your console…
…to see document frames available. Every frame is a space where you can load new resources and execute scripts. A lot of the time, 3rd party services will require you to inject another frame into your main document. Make absolutely sure you know which frames are in your page and for what reason. These are a great place for malicious users to execute potentially unsafe scripts.
Layouts / sec
The Layout event is where your browser reads your page and it’s attached styles and determines the size and placing of your DOM elements. This step in delivering a rendered frame to the browser screen is called Layout (not only for Chrome, but for Opera, Safari, and IE as well). As expected, a large number of DOM nodes will effect the speed of your Layout. You want to aim for the minimum possible number of Layout events in your page.
Changes to your element’s width, height, or position (top, right, bottom, left) will trigger a Layout.
You’re best off using flexbox as it’s faster than older layout methods (think back to float:left).
Javascript wise, keep in mind a style read is done at the start of a frame. Programatically adding a class and then reading (for example) the height of that DOM element produces another, often unnecessary Layout.
Also, imagine setting the height of a group of paragraphs. Looping over a list of DOM elements and setting a style value for each of them will trigger what is called a forced synchronous layout, once for each element. This is called layout trashing. To avoid this you can use a (framework agnostic) library like fastdom, which helps you by batching your style reads and writes, effectively doing away the layout trashing you were about to dish out.
Style recalcs / sec
This metric is more often then not connected with the number of layouts per second.
Changing the DOM, through adding and removing elements, changing attributes, classes, or through animation, will all cause the browser to recalculate element styles and, in many cases, layout (or reflow) the page, or parts of it. This process is called computed style calculation.
As stated above by Paul Lewis, these are the scenarios to look out for when your style recalculations per second are running high enough to choke either the CPU or the GPU, causing your animations to stutter, and overall user experience to fall below an acceptable threshold. And no, an “acceptable threshold” isn’t as subjective as you’d think. I’ve already mentioned the RAIL model — it’s well worth exploring if you’re dealing with the performance of user interfaces.
What you can do here is to start with implementing some best practices, like using the BEM naming convention for your CSS and try to trim unnecessary DOM elements — simplifying your HTML will reduce necessary style recalculations.
Conclusion
What I’ve tried to achieve here is just a simple overview of what the Performance Monitor tracks along with some basic, common sense insights which can get you started on actually using it. Knowing what each metric represents is only half of the problem. Knowing what to do about it in a full scale application context is a different matter. Read up on all the different tools Chrome offers — knowing how your browser works, and what some common types of npm packages leverage is the only way to really take control of your app’s architecture and to bring informed decisions to the table. The old, “I’ve done it this way before” is a poor argument on it’s own. Do your research.
Also, if you think you have some insights into Performance monitor metrics I may have left out or misinterpreted feel free to discuss in the comments — the subject is broad and can be approached in a myriad of ways. If you want to reach out or just grow your network — you can find me on twitter and linkedin.