Find what JavaScript variables are leaking into the global scope

Feb 14, 2022

Detecting variables that are mistakenly or unknowingly added to the global scope can be helpful to debug your apps and avoid naming collisions. The more a web app and its dependencies grow, 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 global name collisions).

In this post, I’ll show you how to find what variables have been added 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, the globalThis 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 the window 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 a web page.
As an example, the following (purposely bad) code is adding multiple variables to the global scope (e.g., jQuery is added by the library itself, i is added because the script is not using "use scrict", etc.).

<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.

too many globals

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:

  1. Create a disposable iframe pointing it at about:blank (to ensure the window object is in a clean state).
  2. Inspect the iframe window object and store its global variable names.
  3. 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 👍

runtime globals snippet

For a more complex version of the script, I created this Gist:

/**
* RuntimeGlobalsChecker
*
* You can use this utility to quickly check what variables have been added (or
* leaked) to the global window object at runtime (by JavaScript code).
* By running this code, the globals checker itself is attached as a singleton
* to the window object as "__runtimeGlobalsChecker__".
* You can check the runtime globals programmatically at any time by invoking
* "window.__runtimeGlobalsChecker__.getRuntimeGlobals()".
*
*/
window.__runtimeGlobalsChecker__ = (function createGlobalsChecker() {
// Globals on the window object set by default by the browser.
// We collect them to then filter them out of from the list of globals (since
// we don't care about them).
// They're populated by "collectBrowserGlobals()" and will contain globals such
// as "location" and "localStorage".
let browserGlobals = [];
// Known globals on the window object that we can safely ignored.
// This list should be populated manually after trial and errors.
const ignoredGlobals = ["__runtimeGlobalsChecker__"];
/**
* Collect the global variables added to the window object by the browser by
* creating a temporary iframe and checking what global variables the browser
* adds on it.
* @returns {string[]} - List of globals added added by the browser
*/
function collectBrowserGlobals() {
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
return browserGlobals;
}
/**
* Return the list of globals added at runtime (by JavaScript).
* @returns {string[]} - List of globals added at runtime (by JavaScript)
*/
function getRuntimeGlobals() {
// If we haven't collected the browser globals yet, do it now.
if (browserGlobals.length === 0) {
collectBrowserGlobals();
}
// Grab all the globals filtering out variables we don't care about (noise).
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
const isIgnored = ignoredGlobals.includes(key);
return !isFromBrowser && !isIgnored;
});
return runtimeGlobals;
}
return {
getRuntimeGlobals,
};
})();


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.