Investigating Memory Leaks in Vue Applications With Chrome Memory Tools
When you build a complex Vue application consisting of many views with deeply nested components hierarchy, very soon you will encounter memory issues.
Modern web applications consume a lot of computer resources. A blog application in which I type this text currently consumes 130 megabytes of memory. On today's scale, it's a pretty decent result. On my laptop equipped with 8 gigabytes of RAM, I start noticing performance problems when a browser tab exceeds somewhere around one gigabyte of allocated memory. When you build a complex Vue application consisting of many views with deeply nested components hierarchy, very soon you will encounter memory issues. Here is a quick overview of what you can do with Chrome devtools' utility to analyze and improve the application's memory consumption.
On some early stage of SQL Studio development, I've noticed a subtle performance decrease after intensively switching between some views. The first thing to do here is to check application memory usage. In Chrome, tabs memory usage can be discovered in the built-in task manager: press Shift+Esc or go to the Chrome main menu and select More tools > Task manager. Right click on the header and select JavaScript Memory. Notice a number in the column, then experiment with the application and check if memory consumption grows. In my case after some steps repetition, I've concluded a 50% increase in memory usage!
There is a good guide from Chrome devtools team on how to fix memory problems. To sum it up, a frequent source of memory leaks in web applications are detached DOM elements that can't be processed by the garbage collector. There is a special tool designed to help in memory leaks investigation, devtools' memory profiles.
Heap snapshot gives an overview of the JavaScript objects residing in memory and relationships between them. Objects that hold references are called "retainers".
The number after the @ character is the objects’ unique ID, allowing you to compare heap snapshots on a per-object basis. What does each constructor group mean you can read here.
If we filter class by value detached, we'll get a list of all elements detached from DOM tree along with objects that directly on indirectly reference these elements (Retainers). There is a good description of distance, shallow and retained size and other terms in the official documentation.
You can select an item and then type $0 in the console to print the element ($0..$4 reference to latest items selected in elements inspector or heap profile, check console utilities API reference).
You can also check the Retainers tree and try to find which objects prevent the detached element from being garbage collected. This is not always a memory leak, elements are also preserved for optimization reasons.
Sometimes, detached elements are a side effect of chrome extensions. For clarity, it's better to disable extensions during the memory leaks investigation (incognito mode can be helpful if you don't want to use a separate browser or don't want to spend time manually disabling extensions).
Also, pay attention to a webpack's development mode, which features like hot module reloading could interfere with the application's normal execution, increase heap allocation and potentially produce leaks. It's better to hunt leaks running the production build of the application.
Suspect objects that retain a huge memory volume, that can give a clue to where the leak happens.
It's not obvious how to find real retainment path until you are very familiar with devtools' heap snapshot internals, V8, Blink rendering engine and chromium overall.
A common approach that I've developed while working on Vue application performance can be simplified into three steps: identify, prove, fix.
First, find in which circumstances memory consumption grows. If you use a router or dynamic components, it's the first place to check. If you suspect a particular component, isolate the component and experiment. Try disabling children components or some features. A common source of memory leaks are incorrect destructing and unhandled contexts. If you call a 3rd-party component constructor inside a component initialization hook, check that you correctly unregister everything in the beforeDestroy hook.
Another potential source of memory wastefulness is a Vuex state. A state that includes complex structures like multidimensional arrays is a common cause for extensive memory consumption, especially if it undergoes many mutations during the application lifecycle.