Detecting global variable leaks can be helpful to debug your apps and avoid collisions in the global scope. The more a web app grows, the more having a good understanding of what’s happening in the global scope becomes important (e.g., to ensure multiple libraries — or even multiple apps! — can coexist on the page without collisions).
In this post, I’ll show you how to find variables that have been added or leaked into the global scope at runtime in web apps (thanks to @DevelopSean for introducing me to this trick at InVision).
Disclaimer : in this post, I’ll reference global scope using the
window
property. Please notice that, in most cases, theglobalThis
property would be a better candidate to access the global object since it works in different JavaScript environments. That said, this post is specific to web (non-worker) contexts, so I think using thewindow
term here makes it easier to follow.
Let’s say you want to check what global variables are being added to the window
object on this page (with some code that looks bad on purpose):
<html>
<body>
<h1>Hello world!</h1>
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.js"></script>
<script>
function doSomethingTwice() {
for (i = 0; i <= 2; i++) {
const myString = `hello-world-${i}`;
// Let's imagine we're going to do something with myString here...
}
}
doSomethingTwice();
</script>
</body>
</html>
Typically, you’d probably open the DevTools console and inspect the window
object looking for suspicious variables.
This approach can work, but… it’s a lot of work. The browser and the JavaScript engine themselves add a bunch of globals on the window
object (e.g., JavaScript APIs such as localStorage
, etc.), so finding globals introduced by our code is like looking for a needle in a haystack.
One possible way to get around this issue is to grab a list of all the default globals and filter them out from the window
object by running a similar snippet in the DevTools console:
const browserGlobals = ['window', 'self', 'document', 'name', 'location', 'customElements', 'history', 'locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', 'toolbar', 'status', 'closed', 'frames', 'length', 'top', ...];
const runtimeGlobals = Object.keys(window).filter(key => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals)
Doing so should work, but it leaves two open questions:
- How do you get the
browserGlobals
variables? - Between cross-browser differences and JavaScript API updates, maintaining the
browserGlobals
list can quickly get hairy. Can we make it better?
To answer both questions, we can generate the browserGlobals
list programmatically by populating it with the globals of a pristine window
object.
There are a couple of ways to do it, but to me, the cleanest approach is to:
- Create a disposable iframe pointing it at
about:blank
(to ensure thewindow
object is in a clean state). - Inspect the iframe
window
object and store its global variable names. - Remove the iframe.
(function () {
// Grab browser's default global variables.
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
const browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
// Get the global variables added at runtime by filtering out the browser's
// default global variables from the current window object.
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals);
})();
Run the snippet above in the console, and you’ll finally see a clean list with of the runtime variables 👍
For a more complex version of the script, I created this Gist:
A couple of final notes:
- This utility can easily run in a Continuous Integration context (e.g., in E2E tests using Cypress) to provide automated feedback.
- I recommend running this utility in browser tabs with no extensions: most browser extensions inject global variables in the
window
object, adding noise to the result (e.g.,__REACT_DEVTOOLS_BROWSER_THEME__
, etc. from the React DevTools extension). - To avoid repeatedly copy/pasting the global checker code in your DevTools console, you can create a JavaScript snippet instead.
Top comments (0)