Debugging memory leaks in web apps is hard. The tooling exists, but it’s complicated, cumbersome, and often doesn’t answer the simple question: Why is my app leaking memory?
Because of this, I’d wager that most web developers are not actively monitoring for memory leaks. And of course, if you’re not testing something, it’s easy for bugs to slip through.
When I first started looking into memory leaks, I assumed it was a rare thing. How could JavaScript – a language with an automatic garbage collector – be a big source of memory leaks? But the more I learned, the more I suspected that memory leaks were actually quite common in Single Page Apps (SPAs) – it’s just that nobody is testing for it!
Since most web developers aren’t fiddling with the Chrome memory tools for the fun of it, they probably won’t notice a leak until the browser tab crashes with an Out Of Memory error, or the page slows down, or someone happens to open up the Task Manager and notice that a website is using many megabytes (or even gigabytes!) of memory. But at that point, it’s gotten bad enough that there may be multiple leaks on the same page.
I’ve written about memory leaks in the past, but my advice basically boils down to: “Use the Chrome DevTools, follow these dozen tedious steps, and then maybe you can figure out why your page is leaking.” This is not a great developer experience, and I’m sure many readers just shook their heads in despair and moved on. It would be much better if a tool could find memory leaks automatically.
That’s why I wrote fuite
(French for “leak”). fuite
is a CLI tool that you can point at any URL, and it will analyze the page for memory leaks:
npx fuite https://example.com
That’s it! By default, it assumes that the site is a client-rendered SPA, and it will crawl the page for internal links (such as /about
or /contact
). Then, for each link, it runs the following steps:
- Click the link
- Press the browser back button
- Repeat to see if memory grows
If fuite
finds any leaks, it will show which objects are suspected of causing the leak:
Test : Go to /foo and back Memory change: +10 MB Leak detected: Yes Leaking objects: | Object | # added | Retained size increase | | ----------------- | ------- | ---------------------- | | HTMLIFrameElement | 1 | +10 MB | Leaking event listeners: | Event | # added | Nodes | | ------------ | ------- | ------ | | beforeunload | 2 | Window | Leaking DOM nodes: DOM size grew by 6 node(s)
To do this, fuite
uses the basic strategy outlined in my blog post. It will launch Chrome, run some scenario n number of times (7 by default) and see if any objects are leaking a multiple of n times (7, 14, 21, etc.).
fuite
will also analyze any Arrays, Objects, Maps, Sets, event listeners, and the overall DOM to see if any of those are leaking. For instance, if an Array grows by exactly 7 after 7 iterations, then it’s probably leaking.
Testing real-world websites
Somewhat surprisingly, the “basic” scenario of clicking internal links and pressing the back button is enough to find memory leaks in many SPAs. I tested fuite
against the home pages for 10 popular frontend frameworks, and found leaks in all of them:
Site | Leak detected | Internal links | Average growth | Max growth |
---|---|---|---|---|
Site 1 | yes | 8 | 27.2 kB | 43 kB |
Site 2 | yes | 10 | 50.4 kB | 78.9 kB |
Site 3 | yes | 27 | 98.8 kB | 135 kB |
Site 4 | yes | 8 | 180 kB | 212 kB |
Site 5 | yes | 13 | 266 kB | 1.07 MB |
Site 6 | yes | 8 | 638 kB | 1.15 MB |
Site 7 | yes | 7 | 1.37 MB | 2.25 MB |
Site 8 | yes | 15 | 3.49 MB | 4.28 MB |
Site 9 | yes | 43 | 5.57 MB | 7.37 MB |
Site 10 | yes | 16 | 14.9 MB | 186 MB |
In this case, “internal links” refers to the number of internal links tested, “average growth” refers to the average memory growth for every link (i.e. clicking it and then pressing the back button), and “max growth” refers to whichever internal link was leaking the most. Note that these numbers don’t include one-time setup costs, as fuite
does one preflight iteration before the normal 7 iterations.
To confirm these results yourself, you can use the Chrome DevTools Memory tab. Here is a screenshot of the worst-performing site from my set, where I click a link, press the back button, take a heap snapshot, and repeat:

On this particular site, memory grows by about 6 MB every time you click a link and go back.
To avoid naming and shaming, I haven’t listed the actual websites. The point is just to show a representative sample of some popular SPAs – the authors of those websites are free to run fuite
themselves and track down these leaks. (Please do!)
Caveats
Note, though, that not every leak in an SPA is an egregious problem that needs to be addressed. SPAs need to, for example, maintain the focus and scroll state to properly support accessibility, which means that there may be some small metadata that is stored for every page navigation. fuite
will dutifully report such leaks (because they are leaks), but it’s up to the developer to decide if a tiny leak is worth chasing or not.
Some memory growth may also be due to browser-internal changes (such as JITing), which the web page can’t really control. So the memory growth numbers are an imperfect measure of what you stand to gain by fixing leaks – it could very well be that a few kBs of growth are unavoidable. (Although fuite
tries to ignore browser-internal growth, and will only say “leaks detected” if there is actionable advice for the web developer.)
In rare cases, some memory growth may also be due to outright browser bugs. While analyzing the sites above, I actually found one (Site #4) that seems to be suffering from this Chrome bug due to <img loading="lazy">
not being unloaded. Unfortunately it’d be hard for fuite
to detect browser bugs, so if you’re mystified by a leak, it’s good to cross-check against other browsers!
Also note that it’s almost impossible for a Multi-Page App (MPA) to leak, because the browser clears memory on every page navigation. (Assuming no browser bugs, of course.) During my testing, I found two frontend frameworks whose home pages were MPAs, and unsurprisingly, fuite
couldn’t find any leaks in them. These were excluded from the results above.
Memory leaks are more of a concern for SPAs, where memory isn’t cleared automatically on each navigation. fuite
is primarily designed for SPAs, although you can run it on MPAs too.
fuite
currently only measures the JavaScript heap memory in the main frame of the page, so cross-origin iframes, Web Workers, and Service Workers are not measured. Something like performance.measureUserAgentSpecificMemory()
would be more accurate, but it’s only available in cross-origin isolated contexts, so it’s not practical for a general-purpose tool right now.
Other memory leak scenarios
The “crawl for internal links” scenario is just the default one – you can also build your own. fuite
is built on top of Puppeteer, so for whatever scenario you want to test, you essentially just need to write a Puppeteer script to tell the browser what to do. Some common scenarios you might test are:
- Open a modal dialog and then close it
- Hover over an element to show a tooltip, then mouse away to dismiss it
- Scroll through an infinite-loading list, then navigate away and back
- Etc.
In each of these scenarios, you would expect memory to be the same before and after. But of course, it’s not always so simple with web apps! You may be surprised how many of your dialogs and tooltips are harboring memory leaks.
To analyze leaks, fuite
captures heap snapshot files, which you can load in the Chrome DevTools to inspect. It also has a --debug
mode that you can use for more fine-grained analysis: stepping through the test as it’s running, debugging the browser in real-time, analyzing the leaking objects, etc.
Under the hood, fuite
is a fairly basic tool, and I won’t claim that it can do 100% of the work of fixing memory leaks. There is still the human component of figuring out why your objects were allocated and retained, and then finding a reasonable fix. But my goal is to automate ~95% of the work, so that it actually becomes achievable to fix memory leaks in web apps.
You can find fuite
on GitHub. Happy leak hunting!
Update: I made a video tutorial showing how to debug memory leaks with fuite
.
Posted by Fuite: a tool for finding memory leaks in web apps - The web development company on December 17, 2021 at 8:49 AM
[…] Article URL: https://nolanlawson.com/2021/12/17/introducing-fuite-a-tool-for-finding-memory-leaks-in-web-apps/ […]
Posted by Wiktor Wiśniewski on December 17, 2021 at 1:08 PM
This is epic! Just found some leaks on my blog. Nolan – you rock!
Posted by Nolan Lawson on December 17, 2021 at 1:14 PM
Awesome! That’s the best news I could hear. :) Glad it helped!
Posted by Introducing fuite: a tool for finding memory leaks in web apps - The web development company on December 17, 2021 at 5:54 PM
[…] submitted by /u/pimterry [link] [comments] Source: […]
Posted by Ralph Haygood on December 17, 2021 at 6:06 PM
Very helpful. Thanks.
I see one kind of output I don’t immediately understand. For example:
Really, memory change is negative?
No big deal, just curious what that means. (Some idiosyncrasy of my code?)
Posted by Nolan Lawson on December 17, 2021 at 8:55 PM
Hm, that may be a bug. Is this a multi-page app? Typically negative memory changes should only occur with multi-page apps. I’m not sure how it’s possible for it to show leaking collections in that case, though.
Posted by Ralph Haygood on December 17, 2021 at 9:34 PM
It’s what might be called a pseudo-SPA using Vue and Rails with Turbolinks. (In case you aren’t acquainted with Turbolinks: it converts page loads into XHRs, so the JavaScript environment persists across them.) fuite does find memory leaks on another page of the same app. (They turn out to be due to a known defect of Vue’s so-called functional components in development environments with hot module reloading, so they disappear when I point fuite at the production version of the app.)
Posted by Nolan Lawson on December 18, 2021 at 11:57 AM
Interesting, maybe your Turbolinks site truly has no leaks. If it lost 147 kB that’s not unheard of; something may have gotten GC’ed between the start and the end. (Although
fuite
runs GC before every heap snapshot, browsers and web pages are complicated, so maybe some cleanup logic ran after that.)As for the “scrollPosition” object, it’s probably a “true” leak (the SPA keeping track of the scrollPosition for every navigation), but so small that it doesn’t actually cause a memory increase. (This makes sense, especially if it’s just storing an integer or something.) I’d just ignore it.
You can try re-running with
--iterations 13
or--iterations 17
to see if you get similar results.Posted by Ralph Haygood on December 17, 2021 at 9:46 PM
if you want to try it, the development version is https://ralph.msrlims.net.
Posted by Ralph Haygood on December 18, 2021 at 6:19 AM
Sorry this didn’t thread properly. I thought I was replying to your (Nolan Lawson’s) reply above beginning “Hm, that may be a bug.”
To add a bit, for whatever it’s worth: the negative memory changes happen with the development version of the app, not the production version, for which every test yields a small positive change. The latter is partly due to Turbolinks caching, which I experimented with turning off to see what difference it made. The production version is at
https://staging.msrlims.net
As the subdomain indicates, this is really a staging version, which is isomorphic to the true production version.
Posted by Nolan Lawson on December 18, 2021 at 12:07 PM
Ah okay. Yeah I analyzed it (here’s how I do it), and the leaking object is just some object that seems to be holding onto the scroll position or something. Probably this is Turbolinks doing this.
This object grows by 2 every time you navigate somewhere and then go back. But since it’s only leaking a string and an empty object, it’s probably so small that it doesn’t matter.
When I test, I’m seeing memory changes of between roughly -70kB and +40kB. These numbers are tiny, so it’s probably just noise and you can ignore it. :)
Posted by Ralph Haygood on December 18, 2021 at 12:58 PM
Thanks.
Posted by Nolan Lawson on December 18, 2021 at 1:41 PM
I was curious, so I actually managed to find the line in Turbolinks that is adding to the object. It’s called
getRestorationDataForIdentifier
, and it’s callingthis.restorationData[identifier] = {};
which is increasing the size of therestorationData
object.Posted by 🚀10 Trending projects on GitHub for web developers - 24th December 2021 - DEV Community on December 24, 2021 at 12:44 PM
[…] Introductory blog post […]
Posted by Young Reacts #159 – youngk.im on December 27, 2021 at 7:13 AM
[…] Introducing fuite: a tool for finding memory leaks in web apps […]
Posted by Revision 518: Browser-Fundstücke | Working Draft on February 27, 2022 at 9:01 PM
[…] Fuite […]
Posted by Randell Jesup on April 1, 2022 at 8:10 AM
Pages can also leak (badly) when “idle” sitting on a page, or scrolling around it in, not just clicking on things. For example, a very popular anti-clickfraud supplier’s code, embedded on tons of high-profile sites, was saving encoded strings representing some type of state. After letting (say) WashingtonPost.com sit for a day, it would have created (say) 100K identical several-hundred-character-long strings. I managed to eventually contact someone technical there, and they promised to fix it. Took 6-9 months for them to actually do so….
Many sites leak large objects in maps or arrays over time.
Facebook used to leak “<div” strings by the 10’s of thousands if you lead a message up for hours or days. Sites with things like live feeds of stuff sometimes will never clean them out (i.e. they expand forever). Etc.
Posted by Improving Firefox stability with this one weird trick - Mozilla Hacks - the Web developer blog on November 26, 2022 at 1:20 PM
[…] There’s also another angle to this: Firefox is made up of several processes and can survive losing all of them but the main one. Delaying a main process crash might lead to another process dying if memory is tight. This is good because it would free up memory and let us resume execution, for example by getting rid of a web page with runaway memory consumption. […]