Skip to main content

Detect Leaks in a Demo App

This is a tutorial demonstrating how to detect detached DOM elements with memlab.

Set up the Example Web App Under Test

The demo app leaks detached DOM elements when you click the "Create detached DOMs" button. Each click creates 1024 detached DOM elements, which are referenced by the window object.

memlab run result

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall web_perf_infra */import Link from 'next/link';import React from 'react';export default function DetachedDom() {  const addNewItem = () => {    if (!window.leakedObjects) {      window.leakedObjects = [];    }    for (let i = 0; i < 1024; i++) {      window.leakedObjects.push(document.createElement('div'));    }    console.log(      'Detached DOMs are created. Please check Memory tab in devtools',    );  };  return (    <div className="container">      <div className="row">        <Link href="/">Go back</Link>      </div>      <br />      <div className="row">        <button type="button" className="btn" onClick={addNewItem}>          Create detached DOMs        </button>      </div>    </div>  );}

Source file: packages/e2e/static/example/pages/examples/detached-dom.jsx

1. Clone Repo

To run the demo web app on you local machine, clone the memlab github repo:

git clone git@github.com:facebook/memlab.git

2. Run the Example App

Once you have cloned the repo on your local machine, run the following commands from the root directory of the Memlab project:

npm install && npm run build
cd packages/e2e/static/example
npm install && npm run dev

This will spin up an example Nextjs app. Let's make sure it is running by visiting from your browser http://localhost:3000:

note

The port number :3000 may be different in your case.

Find Memory Leaks

1. Create a Scenario File

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @nolint
* @oncall web_perf_infra
*/

// memlab/packages/e2e/static/example/scenario/detached-dom.js
/**
* The initial `url` of the scenario we would like to run.
*/
function url() {
return 'http://localhost:3000/examples/detached-dom';
}

/**
* Specify how memlab should perform action that you want
* to test whether the action is causing memory leak.
*
* @param page - Puppeteer's page object:
* https://pptr.dev/api/puppeteer.page/
*/
async function action(page) {
const elements = await page.$x(
"//button[contains(., 'Create detached DOMs')]",
);
const [button] = elements;
if (button) {
await button.click();
}
// clean up external references from memlab
await Promise.all(elements.map(e => e.dispose()));
}

/**
* Specify how memlab should perform action that would
* reset the action you performed above.
*
* @param page - Puppeteer's page object:
* https://pptr.dev/api/puppeteer.page/
*/
async function back(page) {
await page.click('a[href="/"]');
}

module.exports = {action, back, url};

Let's save this file at ~/memlab/scenarios/detached-dom.js.

2. Run memlab

This may take about a few minutes:

memlab run --scenario ~/memlab/scenarios/detached-dom.js

3. Debug Leak Trace

For each leaked object group, memLab prints one representative leak trace.

memlab run result

Let's break down the results from the top to bottom:

Part-1: Browser interaction breadcrumb shows the browser interactions (navigations) memlab performed as specified in our scenario file.

  • page-load[6.5MB](baseline)[s1] - the JavaScript heap size was 6.5MB on initial page load. The baseline heap snapshot will be saved as s1.heapsnapshot on disk.
  • action-on-page[6.6MB](baseline)[s2] - After clicking the "Create detached DOMs" button, the heap size increased to 6.6MB.
  • revert[7MB](final)[s3] - The web page finally reached 7MB after navigating away from the page that triggered the memory leak.

Part-2: Overall summary of the leak trace

  • 1024 leaks - There were 1024 leaked objects. Line 12 of the example app created 1024 detached DOM objects in the for loop.
  • Retained size - the aggregated retained sizes of the leaked objects cluster is 143.3KB (memory leaks are grouped together based on the similarity of retainer traces).

Part-3: Detailed representative leak trace for each leak cluster

note

A leak trace is an object reference chain from the GC root (the entry objects in a heap graph from which garbage collectors traverse the heap) to a leaked object. The trace shows why and how a leaked object is still kept alive in memory. Breaking the reference chain means the leaked object will no longer be reachable from the GC root, and therefore can be garbage collected.

By following the leak trace one step at a time from the native Window (i.e. the GC root) downward, you will be able to find a reference that should be set to null (but it wasn't due to a bug).

  • map - This is the V8 HiddenClass (V8 uses this internally to store meta information about the shape of an object and a reference to its prototype - see more here) of the object being accessed - for the most part this is a V8 implementation detail and can be ignored.

  • prototype - This is the instance of the Window class.

  • leakedObjects - This shows that leakedObjects was a property of the Window object with size 148.5KB pointing to an Array object.

  • 0 - This shows that a detached HTMLDIVElement (i.e. a DOM element that is not currently connected to the DOM tree) is stored as the first element of the leakedObjects array (Since it is overwhelming to show all 1024 leak traces, Memlab only prints one representative leak trace. i.e. property 0 instead of properties 0->1023)

    In short, the leak trace path from window object to leaked object is:

[window](object) -> leakedObjects(property) -> [Array](object)
-> 0(element) -> [Detached HTMLDIVElement](native)

which matches the leaking code in the example:

window.leakedObjects = [];
for (let i = 0; i < 1024; i++) {
window.leakedObjects.push(document.createElement('div'));
}