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.