Conf42 JavaScript 2023 - Online

Analyze the JS Heap and detect Memory Leaks

Video size:

Abstract

Memory leaks or high memory consumption are the number one reason for crashing browser sessions. With this talk I will point out the theoretical concepts of javascripts memory consumption, the garbage collection process, the memory heap and memory leak identification.

Summary

  • This talk will be about JavaScript memory consumption and how to identify JavaScript memory leaks. There are three main contributors that contribute to the amount of memory of our application. Just drop with the new chrome where the chrome browser can tell you by hovering over your tab how much memory it uses.
  • The memory heap is an interconnected graph. It not only stores objects but also their references to other objects. The retained size gives us information about everything this node will release when we remove this specific object from the memory. Distance is telling you the distance the garbage collection collector needs to travel.
  • The memory tab of the chrome browser allows you to inspect the memory of our JavaScript applications. If you select one of the nodes in the heap snapshot, you will be given a retainers list. This will tell you objects that still rely on this specific node. We will go over memory leak detection afterwards.
  • There is a tool in the chrome devtools which says layers. Select your layer via the layer tool and it will give you the reason why it was composited into a new layer. This is a nice tool to inspect how much memory your layers are consuming and how much layers your application actually have.
  • Memory leaks are something that is stored and used, stored but not used anymore by your application. Worst case is when you repeatedly allocate memory without cleaning up. What causes memory leaks? Console logging and leftover subscriptions.
  • We will use again the performance monitor to observe memory consumption over time. We should never forget to trigger the garbage collector before we analyze our heap. It totally depends on your system load and your system set up whatsoever.
  • Microsoft Edge browser added a new tool to their devtools called the detached elements tool. It is dramatically helping in finding memory leaks based off component oriented frameworks. By clicking this line of code we will go directly to the source where this click listener is coming from.
  • So let's go to our final demo for this. This is now the edge browser. And it looks like that we also leak Dom nodes. Let's transform our anonymous function into something we can afterwards destroy with the remove event listener. Afterwards let's check that the leaks are actually gone.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hello everyone, welcome to my talk sealing the gaps a deep dive into JavaScript neural leak detection. So, as you might guess, this talk will be about JavaScript memory consumption and how to identify JavaScript memory leaks happening in your application. Have you ever seen this specific error message which says r Snap, something went wrong. So the number one reason behind it is most probably too high memory consumption of a given chrome tab. So the browser decides to kill the tab and give you this error message. And the reason behind too much memory consumption can also be memory leaks. So before we go into any detail, let me quickly introduce myself. Hi, I'm Julian Jandel. I'm performance engineer at the company pushbasedio. So let's first talk about memory consumption. What consumes memory in our application? There are three main contributors that contribute to the amount of memory of our application or of the git browser tab running our application. On the first hand, we have JavaScript code we write, then we have the DOM we produce with our templates, and we have the composition layer we produce with our style sheets. So let's first talk about JavaScript. Whenever you store an object, in this case just a simple object with one property which says title Spiderman, we store it in memory. We just said say left movie equals this object, and this object is now stored in memory. As soon as we nullify this object back again, then we release this object from memory. Back then we have the case of immutability. So whenever you create a copy, for example, of the object we have created before, then we create a shallow copy which says okay, we have to copy all the primitive values. In this case we will consume twice memory as before, because movie copy is now its very own copied object and not a reference to the other one anymore, then we have DOM nodes. So for each dom node we create, they are stored as strings, and if they contain attributes and other strings, for example text or other values, then this is also stored in the memory of your browser tab. And finally we have the composition layers. So whenever you have any specific rule that promotes a new layer, for example, if you say will change transform or you use directly the transform property CSS attribute, then you will promote a new layer which consumes memory, but in this case specifically on your GPU. So if your device has a dedicated graphics card, then the memory will be stored on your GPU. This is specifically important for lower end devices like mobile phones, because they most probably have not enough dedicated GPU memory available. So let's inspect the DOM and the JavaScript memory. We go twofold here. So in the first one we want to have a bird's eye view, and then we want to do an in depth analysis. So let's start with the bird's eye view. We have multiple tools that help us here so we can, for example, use the performance monitor of the Chrome devtools, which gives us an overview over a time span about the memory consumption of our JavaScript heap, and about the amount of Dom nodes and the amount of JavaScript event listeners. This is very important because all of them contribute as well to the memory of our Chrome tab in total. Then we have the task manager, which also gives you an indicator about the memory footprint. This doesn't have to be completely in line with what the performance monitor tells you, because the memory footprint contains more information than just those metrics. We can see the JavaScript memories, so the JS heap size here in the last column where it says JavaScript memory. And finally, this is a very new feature. Just drop with the new chrome where the chrome browser can tell you by hovering over your tab how much memory it uses. So you do not have to open the task manager anymore, you can simply hover over your tab bar. And this is a pretty cool new feature. So all of those metrics give you just an indication about how much memory your application right now uses, or over a very tiny time span. But let's go into an in depth analysis. Let's find out what happens inside there and how the data is stored in there. So before we go into the in depth analysis, let's talk about the terminology, because we have to introduce some words here. The first and most important one, I guess is the memory heap. So the memory heap is an interconnected graph, which means your objects are not only simple objects, but they only have references to each other. So if you store a reference from one object to another, then this will be also part of the memory heap. And if you want to remove objects from the memory heap, it has to traverse the whole graph in order to find anything. So it not only stores objects but also their references to other objects, as well as primitive values belonging to different objects. Then we have object sizes, and this is threefold. We have the shallow size which describes only the size of this very specific one object we are taking a look at. Then we have the retained size, which gives us information about everything this node will release when we remove this specific object from the memory. So also taking account into everything that relates to this object and not only the object itself. So this is the metric which is most important when you want to prioritize after which nodes are most important to release from the memory because the retained size gives you the information. If I remove this one from the memory, then retained size will be the amount of memory I save from that. And finally we have the distance. Distance is telling you the distance the garbage collection collector needs to travel. So the path the garbage collector need to travel over the graph in order to find a given node and to finally release it within the next garbage collecting cycle. So after all this terminology, let's find out which tools we have in order to inspect the memory of our JavaScript applications. So first of all we have the memory tab of the chrome browser. You can find it by opening the devtools and then simply selecting the memory tab before you want to start any analysis or create a heat snapshot. You always want to trigger the garbage collector which is indicated by this tiny garbage icon here on top. And then you can take a snapshot here down with the blue button which says take snapshot. This will create now a heap snapshot for you and you will be ported over to an overview which will basically give you an information about all the nodes that are stored in your app. So all the arrays, all the strings, everything that belongs to window, all anonymous functions that are somewhere stored. So basically everything that is accessible in your memory and used by your application is now visible in this snapshot and you can search it and filter it. So let's take a look at how you do that. So there is a little search bar on top where it says class filter and in the demo we will go over next I have a class for example named findme. So if you search for it, it will filter all the summary, all the overview entries and will give you only the entry which says find me and you can inspect it then. So you will see with the tiny f symbol here. This is the id of the node stored in your memory and it will give you also an indication where this object specific object is actually created. As you can also see here, you will be informed about the distance and the shallow size and the retained size. So those three terms we discussed before when inspecting nodes in the heap snapshot, if you select one of those nodes, you will be given a retainers list. And the retainers list is one specific super cool feature to inspect memory leaks which we will use later on because this will tell you objects that still rely on this specific node. So if you want to trace down the memory heap then you search in the retainers list. Which other node is still keeping the one I'm searching for in memory. So let's go on a quick demo where we quickly go over the memory tab capabilities. So here you can see a very simple code example just with this findme class which I was telling about before. So this findme class has just a content property which has just a very large array to have artificially large memory consumption. So we can see some number in our memory heap snapshot. And finally we create a const out of it which says new findme and then we console log it to the console. So let's take a look at how this one looks in our browser. So we have just the title here. We will open now the memory tab. So I've opened the chrome dev tools, we don't need the performance monitor for now, but let's take a look at the memory tab here. So the first thing we want to do is collecting garbage. Then we want to take a heap snapshot, and as we can see, five megabytes, pretty large for just the title. So let's search for findme. And as you can see here now we have found our object which says findme with this specific node id. And when we click it, it will open up the retainers list down here in the bottom and it will point us to a line of code where this is still in use. But in this case this is a stacked example and we have just a global const. So this will not go anywhere from this point. We will go over memory leak detection afterwards. But this is just a cool feature for inspecting which other lines of code are still affecting this findme property. Okay, so let's go back to our presentation. We have now discussed how to inspect or how to get an overview over our properties stored in the JavaScript memory heap via the memory tab in the chrome devtools. What we didn't inspect were our composition layers, but I introduced it before, so let's talk about that as well. So I have an example here from observable HQ from the observable HQ landing page. As you can see here, there's a UI element which just shifts elements on a pane left and right, and if you inspect it via the layer tool and shift the application a little bit, then you can see all of those images and all of those tiles are actually on their very own layers. And what does that mean for our memory consumption? So you have seen this slide before, but I want to emphasize this once again. So there are certain rules that promote a new layer in your application. So whenever you use one of those, it will promote a new CSS layer and this will consume memory on the GPU of your device and how much we can also inspect. So other reasons besides the two we have seen before that promote a layer are for example 3d or perspective changes. A video element always promotes a new layer, canvas elements always promotes new layers, animated opacities or transforms. And if you have a sibling with the lower Z index plus a new stacking index. So how to inspect those layers? How does this work? There is a tool in the chrome devtools which says layers. You can reach it because it's not enabled by default by clicking this three button menu on top and then select more tools and there you find layers. So you can just open the layer tool and it will open this nice overview for you where you can zoom and tilt and pinch into the viewport and see basically a 3d view of the application where all the layers are separately displayed. It also gives you detailed information about the layers. And as you can see here down below, here's a memory estimate and a composition reason. So if you want to find out why this element in your browser was promoted to a new layer, then you can just use this tool to inspect it. Select your layer via the layer tool and it will give you the reason why it was composited into a new layer. And also you see this particular layer consumes seven megabytes of GPU memory. So let's take a look at this one as well. Let's open again the chrome browser. So here we have the observable HQ landing page and I am already scrolled down to the point where we actually want to be. So let me open up again the dev tools. So I have basically the default setup, so it is not available by default here. So I will go here and go to more tools and select the layers tool. And now I have the layers tab available here where all the layers are basically collapsed here, but we can expand it and then we can see all of the layers here in this list. And also we can zoom in here this 3d view and by selecting different movement tools we can turn around our application and bring the application into position where we actually can see the layering is happening. So here you can see it nice and beautiful. So the text actually has a completely different layer than the images here up top, and we can see all of them created here. And if we select for example this specific layer here, we can see a memory estimate of 10.5 megabytes and we can also see a composition reason, compositing reason. So this one specifically has an active accelerated transform animation or transition. So we can go over to the elements panel and confirm this once again. But I guess this tool will be right anyway. So this is a nice tool to inspect how much memory your layers are consuming and how much layers your application actually have. And as I said before, this might not be very important for your desktop users, but for mobile users this can be important, especially if you have very large layers that consume lots of memories. So if we for example, just select the outer document, which is pretty large, we see this one consumes 35 megabytes. So treat your layers with caution and everything will be good. Very cool. So let's go back to finally hit the target of identifying memory leaks. And I've brought again the Osnap logo for you because this is a very funny guy, I think. Okay, so let's first talk about what are memory leaks? In order to fix something, we need to know about what they are, right? So memory leaks essentially are, or is memory that is allocated by your application. So something that is stored and used, stored but not used anymore by your application. So you have a global variable stored somewhere, but you don't use it anymore. And this is basically a memory leaks. And if you do it too often, then this one will be the number one reason for crashing browser sessions, and then you will get the odd snap error. So the worst case is when you repeatedly allocate memory without cleaning up. So for example, if you have a component that you create which creates a memory leak, and you create it multiple times and destroy it multiple times, in this case you will see patterns like this in the performance monitor. And this is where the performance monitor then also shines when inspecting memory leaks. So this is just a nice overview over time, where the starting point starts with 10,000 dom nodes, and in the end, after some interactions, you see all of the metrics are just rising and rising and rising. We get Dom nodes added and added and added and added and added. We get event listeners added, sometimes removed, but in the end added and added and more added, same as the Javascript heap size. And we end up seeing heap size increasing by 50 megabytes, DOm nodes by 70,000, and event listeners by 400. So this is definitely indicating a memory leak. And if you see such a pattern in your application, you should be worried about it because this can end up in a OS map. So what causes memory leaks? Finally? First of all, console logging. So this was very unexpected when I heard it the first time, but it's true and it makes sense if you think about it. So in order to display the value in your console or an object in your console. It has to keep reference to it, right? It doesn't create a copy just on its own. It references the object to finally display it and you can verify it by console log something. Even with the closed console, if you open your console afterwards, it will still be there and print it out. So still with a closed console, you will have a memory leak. Here. If you print out objects, then we have global variables. So whenever you store something on window or just create a random const in any component outside of the component's class scope, then you create something that is not really cleanable anymore. So you create something in a global scope and this will be stored and will be forever there and never clean up. Of course you can reuse it and you won't do it multiple times if you import the component file again. But just so you know, global variables will store memory which cannot be cleaned up. Then leftover subscriptions and this is most probably the number one reason for most memory leaks, like leftover callbacks and subscriptions to for example event listeners or intervals or just other RXJS subscriptions. So whenever you have something that you do not kill which runs forever like an interval or a timer from rxjs, and you reference some other value inside of it, it cannot be released anymore because this forever ongoing callback always keeps a reference to this object. And this means that in our case we create here a new foo object which will always be in memory, but also everything that relates to it. So if Foo has private values like this huge data amount, then of course huge data will also be part of the memory footprint. Same goes for HTML elements. And HTML elements have some specifics here. So they are not only contributing like Javascript values here, because if you end up having an HTML element which you cannot clean up anymore, it will get a detached element. So in this case you just have a reference in a function that is never cleaned up in a global scope to a button that you wanted actually to remove, then the button cannot be really removed. Of course it's not part of the DoM anymore that the user sees, but it will be a detached element. So now we know what memory leaks are. Now let's finally talk about how to detect memory leaks. So we will do follow the same approach as we did before. We will go first into a bird's eye view and then into an in depth analysis. Let's start with the bird's eye view, so this time we can use again the performance monitor to observe memory consumption over time. Of our application. As I've seen before, as I've shown you before, we want to indicate or see those patterns. So those patterns can indicate okay, this situation looks safe, or this situation definitely looks like something we need to dig into. So if we see the pattern on the left side where all of our metrics, or even if it's only one, is only increasing over time and is never ever decreasing, then 100% I can tell you something is wrong, whereas on the other side you see there is a slight increase, but there is also decrease every time. And afterwards, at the last point in time, this is where the garbage collector could release basically everything and we are on par on plane like the one before. This is the perfect scenario you want to see. So then everything is fine. If you have something on the left you should definitely take a look at. So exactly. We should never forget to trigger the garbage collector before we analyze our heap because otherwise the garbage collector is uncontrollable. We have no control about when the garbage collector of our browser decides to collect something, it totally depends on your system load and your system set up whatsoever. So before you do any investigation or something, then please go ahead and trigger the garbage collector annually because otherwise there's something you might didn't want to see. So let's go into the in depth analysis. Analysis so do you remember the detached elements I talked about before where this is very important because most of the time as we develop on front ends, we are tightly coupling our JavaScript code anyway, the DOM elements, as we are working with components that afterwards get dom nodes. And this is really cool because the edge browser, the Microsoft Edge browser with version 93, added a new tool to their devtools, which is called the detached elements tool, which is dramatically helping in finding memory leaks based off component oriented frameworks. So how does this work? So this looks basically the same as the chrome dev tools, but instead of the three dot menu you have a plus icon here and there. You can select the detached elements tool here. If you open it, you want to follow the following approach. So the buttons are in my opinion the wrong order because the first thing you always want to do is triggering the garbage collector. So hitting the trash button here, the trash bin button here as the first button, then we want to read the detached elements. So this is telling the browser to read all detached elements that are still kept in Dom, and afterwards we want to analyze the heap. So first we know about all the detached elements and then we want to deeply analyze where those detached elements belong to. So this will lead us to this list of detached elements after we followed this approach, where each of those elements should have an id. Elements that do not have an id anymore you can safely ignore because they are not part of this heap anymore. On the top right you see a total amount of detached elements found in our current example, and then we can go ahead and select one of those. And this will now be interconnected with the memory tab we have seen before and open up the retainers list. So if you remember, the retainers list will give you information about where this code is actually still in use. And in this example, you see our detached node is a list item. We select its id, it will tell us open up the memory tab down here at the bottom. Then it will show us it is a detached HTML diff element and it will point us directly to the source of leakage. So we can just click basically this line of code here. And we see here context in. So this will definitely mean detached v eight event listener. So this means probably an anonymous function in an event listener, maybe a click event or something. And by clicking this line of code we will go directly to the source where this click listener is coming from, and we can fix it right away. So let's go to our final demo for this. I need to switch now to the edge browser. So this is now the edge browser and this is another stackbus example I am showing here. So this one is slightly more complex than the one before. So let's first open up our dev tools here. And let's start with the performance monitor because we want at first confirm of course, is there a memory leak or is there none? And of course, as I've always said before, we should clear our garbage. As you have seen, we have a slight ditch here now. So this was a good thing to have a clean state. And now we want to go ahead and toggle this button and we can see, okay, now we have 120 megabytes, let's go ahead and click one more, 200, 270, and more and more and more. So we'll see this stair like pattern, and this already indicates a memory leak. But to be sure, we definitely need to collect our garbage here. So let's collect garbage. And yeah, when garbage collection collection is not doing the thing here, then we can be sure that we run into a memory leak here. So let's figure out what is the problem. And it looks like that we also leak Dom nodes. So we can safely say it's kind of related here, the amount of Javascript heap size and the amount of Dom nodes. So let's do a detached elements analysis. I want to collect the garbage again. And then we click here to get our detached elements. So now we have this list here and it says on the first side, object not found in memory. That is because we didn't analyze the heap yet. So when we analyze the memory heap, it will transform or analyze it and transform those into actual ids. And if here is now some leftover which has no id, and this one can be ignored. But all of them have ids, so it looks like all of them would be still kept in memories. And those are actual divs. Of course they are not part of the domno. So if we inspect it, it's completely empty. But they are still here as detached Dom nodes and stored in our memory of the browser. So the size column here is actually not indicating the amount of memory here is stored, but the depth of the node. So if we open it up, we see some track nodes and size is indicating that. So if we now select this id here, then it will open up the memory tab at the bottom, immediately select it, and immediately open up the retainers list and point us directly to the source of leaks. So we see here context in and we see v eight event listener. V eight event listener indicates an event listener of an event of a HTML node. And here we can just click this specific line of code and it will directly point us to our list item implementation here. List item. And we see bold Toggle has an event listener to the event change with an anonymous function. And because this one is probably never destroyed, when we actually toggle our list back to an invisible or destroyed state, that's why it's still kept in memory and that's why we could find it now. So let's go ahead and fix the lines of code that our memory leaks are gone. And afterwards let's check that the leaks are actually gone. Okay, so what I want to do now is I want to transform our anonymous function into something we can afterwards destroy with the remove event listener. Let's first go quickly over the code. So we have this class list item which gets created whenever we toggle this button here. So whenever we click toggle list, we create a bunch of list items. So every dom node you see here is actually a list item class. And this one will create its node template by an item template I have created, clone it. And this is the template we are creating here. Basically a very simple component as a class. Okay, so this specific event listener is now the problematic thing. So what we actually want to do is we want to have for example a destroy method like in angular the Ng destroy method. But we are in plain Javascript field here, so we have no framework taking care of this for us. We will have to do it manually. But on destroy we want to use the bold toggle and use remove event listener and remove our event listener to change in order to fix it. The last thing we need to do here in the list item is to store this function as something we can afterwards reuse. So we want to store it here as a bold change listener exactly like this. And now we can apply it here in our add event listener method, and we can also use it here in the remove event listener as a reference. And now we should be sure when the destroy method is called that our event listener is removed and the leaks is gone. So let's make sure that we also call the destroy method. So here we have the destroy list function which is called whenever we click the toggle button again. So what we want to do is we have the item here already and we want to call the destroy method. So let's save this real quick, open it up in a new browser tab and quickly confirm that the change changes here. So this one looks good. And now we want to confirm also via the performance monitor. So we open it up, we see still memory consumption, but there you go, you could see the memory got released after a couple of clicks because the browser decided to. So now you see not a stair like pattern which goes on forever and forever and forever, but instead it will keep only something in memory, probably even for optimization purposes. But when I now click the garbage collection here manually, then we see the heap size is back there where it should be on the very low level of seven megabytes. Okay, I guess my time ends here. I thank you very much for your time. I hope you enjoyed listening to me and you learned something from this talk. If you have any questions about this talk, please ping me. My email is here and also my Twitter handle, so please reach out to me. Thank you.
...

Julian Jandl

Lead Performance Engineer, Trainer, Consultant @ Push-Based.io

Julian Jandl's LinkedIn account Julian Jandl's twitter account



Join the community!

Learn for free, join the best tech learning community for a price of a pumpkin latte.

Annual
Monthly
Newsletter
$ 0 /mo

Event notifications, weekly newsletter

Delayed access to all content

Immediate access to Keynotes & Panels

Community
$ 8.34 /mo

Immediate access to all content

Courses, quizes & certificates

Community chats

Join the community (7 day free trial)