Find and fix tricky memory leaks caused by detached windows.
What's a memory leak in JavaScript?
A memory leak is an unintentional increase in the amount of memory used by an application over time. In JavaScript, memory leaks happen when objects are no longer needed, but are still referenced by functions or other objects. These references prevent the unneeded objects from being reclaimed by the garbage collector.
The job of the garbage collector is to identify and reclaim objects that are no longer reachable from the application. This works even when objects reference themselves, or cyclically reference each other–once there are no remaining references through which an application could access a group of objects, it can be garbage collected.
let A = {};
console.log(A); // local variable reference
let B = {A}; // B.A is a second reference to A
A = null; // unset local variable reference
console.log(B.A); // A can still be referenced by B
B.A = null; // unset B's reference to A
// No references to A are left. It can be garbage collected.
A particularly tricky class of memory leak occurs when an application references objects that have their own lifecycle, like DOM elements or popup windows. It's possible for these types of objects to become unused without the application knowing, which means application code may have the only remaining references to an object that could otherwise be garbage collected.
What's a detached window?
In the following example, a slideshow viewer application includes buttons to open and close a
presenter notes popup. Imagine a user clicks Show Notes, then closes the popup window directly
instead of clicking the Hide Notes button–the notesWindow variable still holds a reference
to the popup that could be accessed, even though the popup is no longer in use.
<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
let notesWindow;
document.getElementById('show').onclick = () => {
notesWindow = window.open('/presenter-notes.html');
};
document.getElementById('hide').onclick = () => {
if (notesWindow) notesWindow.close();
};
</script>
This is an example of a detached window. The popup window was closed, but our code has a reference to it that prevents the browser from being able to destroy it and reclaim that memory.
When a page calls window.open() to create a new browser window or tab, a
Window object is returned that
represents the window or tab. Even after such a window has been closed or the user has navigated it
away, the Window object returned from window.open() can still be used to access information
about it. This is one type of detached window: because JavaScript code can still potentially access
properties on the closed Window object, it must be kept in memory. If the window included a lot of
JavaScript objects or iframes, that memory can't be reclaimed until there are no remaining
JavaScript references to the window's properties.
The same issue can also occur when using <iframe> elements. Iframes behave like nested windows
that contain documents, and their contentWindow property provides access to the contained Window
object, much like the value returned by window.open(). JavaScript code can keep a reference to an
iframe's contentWindow or contentDocument even if the iframe is removed from the DOM or its URL
changes, which prevents the document from being garbage collected since its properties can still be
accessed.
In cases where a reference to the document within a window or iframe is retained from JavaScript,
that document will be kept in-memory even if the containing window or iframe navigates to a new
URL. This can be particularly troublesome when the JavaScript holding that reference doesn't detect
that the window/frame has navigated to a new URL, since it doesn't know when it becomes the last
reference keeping a document in memory.
How detached windows cause memory leaks
When working with windows and iframes on the same domain as the primary page, it's common to listen
for events or access properties across document boundaries. For example, let's revisit a variation
on the presentation viewer example from the beginning of this guide. The viewer opens a second
window for displaying speaker notes. The speaker notes window listens forclick events as its cue
to move to the next slide. If the user closes this notes window, the JavaScript running in the
original parent window still has full access to the speaker notes document:
<button id="notes">Show Presenter Notes</button>
<script type="module">
let notesWindow;
function showNotes() {
notesWindow = window.open('/presenter-notes.html');
notesWindow.document.addEventListener('click', nextSlide);
}
document.getElementById('notes').onclick = showNotes;
let slide = 1;
function nextSlide() {
slide += 1;
notesWindow.document.title = `Slide ${slide}`;
}
document.body.onclick = nextSlide;
</script>
Imagine we close the browser window created by showNotes() above. There's no event handler
listening to detect that the window has been closed, so nothing is informing our code that it should
clean up any references to the document. The nextSlide() function is still "live" because it is
bound as a click handler in our main page, and the fact that nextSlide contains a reference to
notesWindow means the window is still referenced and can't be garbage collected.
There are a number of other scenarios where references are accidentally retained that prevent detached windows from being eligible for garbage collection:
Event handlers can be registered on an iframe's initial document prior to the frame navigating to its intended URL, resulting in accidental references to the document and the iframe persisting after other references have been cleaned up.
A memory-heavy document loaded in a window or iframe can be accidentally kept in-memory long after navigating to a new URL. This is often caused by the parent page retaining references to the document in order to allow for listener removal.
When passing a JavaScript object to another window or iframe, the Object's prototype chain includes references to the environment it was created in, including the window that created it. This means it's just as important to avoid holding references to objects from other windows as it is to avoid holding references to the windows themselves.
index.html:
<script> let currentFiles; function load(files) { // this retains the popup: currentFiles = files; } window.open('upload.html'); </script>upload.html:
<input type="file" id="file" /> <script> file.onchange = () => { parent.load(file.files); }; </script>
Detecting memory leaks caused by detached windows
Tracking down memory leaks can be tricky. It is often difficult to construct isolated reproductions of these issues, particularly when multiple documents or windows are involved. To make things more complicated, inspecting potential leaked references can end up creating additional references that prevent the inspected objects from being garbage collected. To that end, it's useful to start with tools that specifically avoid introducing this possibility.
A great place to start debugging memory problems is to take a heap snapshot. This provides a point-in-time view into the memory currently used by an application - all the objects that have been created but not yet garbage-collected. Heap snapshots contain useful information about objects, including their size and a list of the variables and closures that reference them.