Skip to main content

Detect Oversized Object

This is a tutorial demonstrating how to detect oversized objects that are not released by a web app.

We recommend reading Detect Leaks in a Demo App, which will walk you through how to interpret memlab results and debug leak traces.

Set up the Example Web App Under Test

The demo app leaks a big array in each React rendering call (through the unregistered event handler and the closure scope chain).

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall memory_lab */import Link from 'next/link';import React, {useEffect} from 'react';export default function OversizedObject() {  const bigArray = Array(1024 * 1024 * 2).fill(0);  const eventHandler = () => {    // the eventHandler closure keeps a reference    // to the bigArray in the outter scope    console.log('Using hugeObject', bigArray);  };  useEffect(() => {    // eventHandler is never unregistered    window.addEventListener('custom-click', eventHandler);  }, []);  return (    <div className="container">      <div className="row">        <Link href="/">Go back</Link>      </div>      <br />      <div className="row">        Object<code>bigArray</code>is leaked. Please check <code>Memory</code>{' '}        tab in DevTools      </div>    </div>  );}

Source file: packages/e2e/static/example/pages/examples/oversized-object.jsx

1. Clone Repo

To run the demo web app on your 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 start an example Next.js app. Verify that it is running by visiting http://localhost:3000 in your browser:

note

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

First Attempt to Find Leaks

1. Create a Scenario File

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall memory_lab */function url() {  return 'http://localhost:3000/';}// action where you suspect the memory leak might be happeningasync function action(page) {  await page.click('a[href="/examples/oversized-object"]');}// how to go back to the state before actionwasync function back(page) {  await page.click('a[href="/"]');}module.exports = {action, back, url};

Let's save this file as ~/memlab/scenarios/oversized-object.js.

2. Run memlab

This will take a few minutes.

memlab run --scenario ~/memlab/scenarios/oversized-object.js

This time memlab didn't find any memory leaks.

The built-in leak detector only considers objects that meet all of the following criteria as memory leaks:

  • The object was allocated by the interaction triggered by the action callback.
  • The object was not released after the interaction triggered by the back callback.
  • The object is a detached DOM element or an unmounted React Fiber node.

Other objects allocated by the action callback may be caches that the web app retains intentionally, so by default memlab does not report them as leaks.

Filter Out Large Objects

A useful rule of thumb is to check for unreleased objects above a certain size threshold (for example, 1MB).

memlab provides an additional leakFilter callback to filter out leaked objects with self-defined rules.

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall memory_lab */function url() {  return 'http://localhost:3000/';}// action where you suspect the memory leak might be happeningasync function action(page) {  await page.click('a[href="/examples/oversized-object"]');}// how to go back to the state before actionwasync function back(page) {  await page.click('a[href="/"]');}// leakFilter is called with each object (node) in browser// allocated by `action` but not released after the `back` callfunction leakFilter(node, _snapshot, _leakedNodeIds) {  return node.retainedSize > 1000 * 1000;}module.exports = {action, back, leakFilter, url};

Now let's rerun memlab with the updated scenario file:

memlab run --scenario ~/memlab/scenarios/oversized-object.js

memlab run result

By examining the leak trace, we can see that the elements in bigArray are being retained because bigArray is captured by the eventHandler closure, which in turn is retained because it is still registered as the handler for an EventListener. If you add a cleanup function to the useEffect as shown below and run the scenario again, memlab will no longer report any leaks.

useEffect(() => {
window.addEventListener('custom-click', eventHandler);
return () => {
// clean up
window.removeEventListener('custom-click', eventHandler);
};
}, []);
note

Alternatively, if you have already run memlab run, you can create a leak-filter.js file and pass it to memlab find-leaks.

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall memory_lab */// leakFilter is called with each object (node) in browser// allocated by `action` but not released after the `back` callfunction leakFilter(node, _snapshot, _leakedNodeIds) {  return node.retainedSize > 1000 * 1000;}module.exports = {leakFilter};

Extract the leakFilter function into its own file, for example ~/memlab/leak-filters/leak-filter-by-retained-size.js:

memlab find-leaks --leak-filter ~/memlab/leak-filters/leak-filter-by-retained-size.js

If you need more advanced filtering logic, here are more examples.