Fighting memory leaks in web applications

When we moved from the development of websites whose pages are formed on the server to the creation of single-page web applications that are rendered on the client, we adopted certain rules of the game. One of them is the accurate handling of resources on the user's device. This means - do not block the main stream, do not ā€œspinā€ the laptop fan, do not put the phoneā€™s battery. We exchanged an improvement in the interactivity of web projects, and the fact that their behavior became more like the behavior of ordinary applications, a new class of problems that did not exist in the world of server rendering.



One such problem is memory leaks. A poorly designed one-page application can easily gobble up megabytes or even gigabytes of memory. It is able to take up more and more resources even when it sits quietly on the background tab. The page of such an application, after capturing an exorbitant amount of resources, may begin to ā€œslow downā€ greatly. In addition, the browser can simply shut down the tab and tell the user: ā€œSomething went wrong.ā€


Something went wrong

Of course, sites that are rendered on the server can also suffer from a memory leak problem. But here we are talking about server memory. At the same time, it is highly unlikely that such applications will cause a memory leak on the client, since the browser clears the memory after each user transition between pages.

The topic of memory leaks is not well covered in web development publications. And despite this, I am almost sure that most non-trivial single-page applications suffer from memory leaks - unless the teams that deal with them have reliable tools to detect and fix this problem. The point here is that in JavaScript it is extremely easy to randomly allocate a certain amount of memory, and then just forget to free this memory.

The author of the article, the translation of which we are publishing today, is going to share with readers his experience in combating memory leaks in web applications, and also wants to give examples of their effective detection.

Why is so little written about this?


First, I want to talk about why so little is written about memory leaks. I suppose here you can find several reasons:

  • Lack of user complaints: most users are not busy closely monitoring the task manager while browsing the web. Typically, the developer does not encounter user complaints until the memory leak is so serious that it causes the inability to work or slow down the application.
  • : Chrome - , . .
  • : .
  • : Ā«Ā» . , , , , -.


Modern libraries and frameworks for developing web applications, such as React, Vue and Svelte, use the component model of the application. Within this model, the most common way to cause a memory leak is something like this:

window.addEventListener('message', this.onMessage.bind(this));

That's all. This is all that is needed to ā€œequipā€ a project with a memory leak. To do this, just call the addEventListener method of some global object (like window, or <body>, or something similar), and then, when unmounting the component, forget to remove the event listener using the removeEventListener method .

But the consequences of this are even worse, since the leak of the whole component occurs. This is due to the fact that the method is this.onMessageattached to this. Along with this component, leakage of its child components occurs. It is very likely that all the DOM nodes associated with this component will leak. The situation, as a result, can get out of control very quickly, leading to very bad consequences.

Here's how to solve this problem:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

Situations in which memory leaks occur most often


Experience tells me that memory leaks most often occur when using the following APIs:

  1. Method addEventListener. This is where memory leaks occur most often. To solve the problem, it is enough to call at the right time removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, Ā«Ā» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . ā€” <body>, document, header footer, .
  4. Promise-, , . , , ā€” , . , , Ā«Ā» , . Ā«Ā» .then()-.
  5. Repositories represented by global objects. When you use something like Redux to control the state of an application , the state store is represented by a global object. As a result, if you deal with such storage carelessly, unnecessary data will not be deleted from it, as a result of which its size will constantly increase.
  6. Infinite DOM growth. If the page implements endless scrolling without the use of virtualization , this means that the number of DOM nodes on this page can grow unlimitedly.

Above, we examined situations in which memory leaks occur most often, but, of course, there are many other cases that cause the problem of interest to us.

Memory leak identification


Now we have moved on to the challenge of identifying memory leaks. To begin with, I donā€™t think that any of the existing tools is very suitable for this. I tried the Firefox memory analysis tools, tried the tools from Edge and IE. Tested even Windows Performance Analyzer. But the best of these tools are still Chrome Developer Tools. True, in these tools there are many "sharp corners", which are worth knowing.

Among the tools that the Chrome developer gives, we are most interested in the profiler Heap snapshotfrom the tab Memory, which allows you to create heap snapshots. There are other tools for analyzing memory in Chrome, but I have not been able to extract special benefits from them in detecting memory leaks.


The Heap snapshot tool allows you to take snapshots of the memory of the main stream, web workers or iframe elements.

If the Chrome tool window looks like the one shown in the previous figure, when you click the buttonTake snapshot, information about all objects in the memory of the selected virtual machine is captured JavaScript of the investigated page. This includes objects referenced inwindow, objects referenced by the callbacks used in the callsetInterval, and so on. A snapshot of memory can be perceived as a ā€œfrozen momentā€ of the work of the investigated entity, representing information about all the memory used by this entity.

After the picture is taken, we come to the next step of finding leaks. It consists in reproducing a scenario in which, according to the developer, a memory leak may occur. For example, it is opening and closing a certain modal window. After the similar window is closed, it is expected that the amount of allocated memory will return to the level that existed before the window was opened. Therefore, they take another picture, and then compare it with the picture taken earlier. As a matter of fact, comparison of images is the most important feature of interest to us Heap snapshot.


We take the first snapshot, then we take actions that can cause a memory leak, and then we take another snapshot. If there is no leak, the size of the allocated memory will be equal.

True, thisHeap snapshotis far from an ideal tool. It has some limitations worth knowing about:

  1. Even if you click on the small button on the panel Memorythat starts garbage collection ( Collect garbage), then in order to be sure that the memory is really cleared, you may need to take several consecutive pictures. I usually have three shots. Here it is worth focusing on the total size of each image - it, in the end, should stabilize.
  2. -, -, iframe, , , . , JavaScript. ā€” , , .
  3. Ā«Ā». .

At this point, if your application is quite complex, you may notice a lot of ā€œleakingā€ objects when comparing snapshots. Here the situation is somewhat complicated, since what can be mistaken for a memory leak is not always the case. Much of what is suspicious is just normal processes for working with objects. The memory occupied by some objects is cleared to place other objects in this memory, something is flushed to the cache, and so that the corresponding memory is not cleared immediately, and so on.

We make our way through the information noise


I found that the best way to break through the noise of information is to repeat the actions that are supposed to cause a memory leak. For example, instead of opening and closing the modal window only once after capturing the first shot, this can be done 7 times. Why 7? Yes, if only because 7 is a noticeable prime. Then you need to take a second shot and, comparing it with the first, find out if a certain object ā€œleakedā€ 7 times (or 14 times, or 21 times).


Compare heap snapshots. Please note that we are comparing image No. 3 with image No. 6. The fact is that I took three shots in a row so that Chrome would have more garbage collection sessions. In addition, note that some objects ā€œleakedā€ 7 times.

Another useful trick is that, at the very beginning of the study, before creating the first picture, perform the procedure once, during which, as expected, memory leak. This is especially recommended if code splitting is used in the project. In such a case, it is very likely that upon the first execution of the suspicious action, the necessary JavaScript modules will be loaded, which will affect the amount of allocated memory.

Now you may have a question about why you should pay special attention to the number of objects, and not to the total amount of memory. Here we can say that we intuitively strive to reduce the amount of "leaking" memory. In this regard, you might think that you should monitor the total amount of memory used. But this approach, for one important reason, does not suit us particularly well.

If something ā€œleaksā€, it happens because (retelling Joe Armstrong ) you need a banana, but you end up with a banana, the gorilla that holds it, and also, in addition, all the jungle. If we focus on the total amount of memory, it will be the same as ā€œmeasuringā€ the jungle, and not the banana that interests us.


Gorilla eating a banana.

Now back to the above example withaddEventListener. A leak source is an event listener that references a function. And this function, in turn, refers to a component that, possibly, stores links to a bunch of good stuff like arrays, strings, and objects.

If you analyze the difference between the images, sorting the entities by the amount of memory they occupy, this will allow you to see many arrays, lines, objects, most of which are most likely not related to the leak. And after all, we need to find the very event listener from which it all began. He, in comparison with what he refers to, takes up very little memory. In order to fix the leak, you need to find a banana, not the jungle.

As a result, if you sort the records by the number of ā€œleakedā€ objects, you will notice 7 event listeners. And maybe 7 components, and 14 subcomponents, and maybe something else like that. This number 7 should stand out from the big picture, since it is, nevertheless, a rather noticeable and unusual number. In this case, it does not matter how many times the suspicious action is repeated. When examining images, if the suspicions are justified, it will be recorded just as many ā€œleakedā€ objects. This is how you can quickly identify the source of a memory leak.

Link Tree Analysis


The tool for creating snapshots provides the ability to view ā€œlink chainsā€ that help you find out which objects are referenced by other objects. This is what allows the application to function. By analyzing such ā€œchainsā€ or ā€œtreesā€ of links, you can find out exactly where the memory was allocated for the ā€œleakingā€ object.


The chain of links allows you to find out which object refers to the "leaky" object. When reading these chains, it is necessary to take into account that the objects located in them below refer to the objects located above.

In the above example, there is a variable calledsomeObjectreferenced in the closure (context) referenced by the event listener. If you click on the link leading to the source code, a fairly understandable text of the program will be displayed:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

If we compare this code with the previous figure, it turns out that contextthe figure is a closure onMessagethat refers to someObject. This is an artificial example . Real memory leaks can be much less obvious.

It is worth noting that the heap snapshot tool has some limitations:

  1. If you save a snapshot file and then upload it again, links to files with code are lost. That is, for example, having downloaded a snapshot, it will not be possible to find out that the event listener closure code is on line 22 of the file foo.js. Since this information is extremely important, saving heap snapshot files, or, for example, transferring them to someone, is almost useless.
  2. WeakMap, Chrome , . , , , , . WeakMap ā€” .
  3. Chrome , . , , , , . , object, EventListener. object ā€” , , , Ā«Ā» 7 .

This is a description of my basic strategy for identifying memory leaks. I have successfully used this technique to detect dozens of leaks.

True, I must say that this guide to finding memory leaks covers only a small part of what is happening in reality. This is just the beginning of the work. In addition, you need to be able to handle the installation of breakpoints, logging, testing corrections to determine if they solve the problem. And, unfortunately, all this, in essence, translates into a serious investment of time.

Automated memory leak analysis


I want to start this section with the fact that I could not find a good approach to automating the detection of memory leaks. Chrome has its own performance.memory API , but for privacy reasons, it does not allow you to collect sufficiently detailed data. As a result, this API cannot be used in production to detect leaks. The W3C Web Performance Working Group previously discussed memory tools , but its members have yet to agree on a new standard designed to replace this API.

In test environments, you can increase the granularity of data output performance.memoryusing the Chrome flag --enable-precise-memory-info. Heap snapshots can still be created using the Chromedriver's own team : takeHeapSnapshot . This team has the same limitations that we have already discussed. It is likely that if you use this command, then, for the reasons described above, it makes sense to call it three times, and then take only what was received as a result of its last call.

Since event listeners are the most common source of memory leaks, Iā€™ll talk about another leak detection technique I use. It consists in creating monkey patches for the API addEventListenerand removeEventListenerin counting the links to check that their number is returning to zero. Here is an example of how this is done.

In the Chrome Developer Tools, you can also use the getEventListeners native API to find out which event listeners are attached to a particular element. This command, however, is available only from the developer toolbar.

I want to add that Matthias Binens told me about another useful Chrome tools API. These are queryObjects . With it, you can get information about all objects created using a certain constructor. Here is some good material on this topic about automating memory leak detection in Puppeteer.

Summary


Searching and fixing memory leaks in web applications is still in its infancy. Here I talked about some techniques that, in my case, performed well. But it should be recognized that the application of these techniques is still fraught with certain difficulties and time-consuming.

As with any performance issues, as they say, a pinch ahead of time is worth a pound. Perhaps someone will find it useful to prepare the appropriate synthetic tests rather than analyze the leak after it has already occurred. And if itā€™s not one leak, but several, then the analysis of the problem can turn into something like peeling onions: after one problem is fixed, another is discovered, and then this process repeats (and all this time, like from onions , tears on eyes). Code reviews can also help identify common leak patterns. But this - if you know - where to look.

JavaScript is a language that provides secure working with memory. Therefore, there is some irony in how easily memory leaks happen in web applications. True, this is partly because of the features of the device user interfaces. You need to listen to a lot of events: mouse events, scroll events, keyboard events. Applying all of these patterns can easily lead to memory leaks. But, striving to ensure that our web applications use the memory sparingly, we can increase their performance and protect them from ā€œcrashesā€. In addition, we thereby demonstrate respect for the resource limits of user devices.

Dear readers! Have you encountered memory leaks in your web projects?


All Articles