Be careful with console logs


Recently I’ve been optimizing a WebVR application written with THREE.js. There were performance issues, mostly caused by huge memory leaks.

“Symptoms” were pretty straightforward – memory consumption was growing while using the app, and at some point it consumed too much memory (few gigabytes, to be more precise) and crashed the browser.

In this article I will share my experience of WebVR app performance optimization. But in order to optimize something, you need to measure it first.

Measuring JavaScript Memory usage

I started from profiling JavaScript memory (JS Heap snapshots) in Chrome. That’s the first thing you’ll find on the internet when it comes to performance in JavaScript.

JavaScript memory represents how much memory the reachable objects on your page are using. It doesn’t store DOM nodes, images, canvases, audio, etc.

I was able to significantly decrease JavaScript memory consumption (by ~50%) by reusing geometries and materials. But I will not cover that in this blog post, since it’s very specific to THREE.js.

Although even after JS memory decreased, there still were huge memory leaks. That’s because JS memory was only tiny part of application Memory footprint.

Memory footprint

Memory footprint represents all the memory allocated for particular tab. It stores DOM nodes, canvases, images, resources used for WebGL rendering.

You can check it in Chrome Task Manager (Shift + Esc).

To prevent memory footprint from growing, you should deallocate memory whenever possible — by removing references to unused objects, DOM nodes, etc. This way garbage collector would know that it can free up memory.

When using THREE.js, you should also dispose geometries, materials and textures. But even after disposing every disposable THREE.js object, my app’s memory footprint kept growing.

Power of two textures

Here’s the thing. THREE.js requires textures size to be power of two (POT), and converts not power of two (NPOT) textures to POT. For example, 1000 is not POT size, so THREE.js will resize it to 1024.

Every time NPOT texture is converted to POT, THREE.js warns about it in console. It looks like this:

Not power of two” warnings in THREE.js“Not power of two” warnings by THREE.js

Console warnings

Did you notice something weird in those warnings? That canvas thing looks very similar to DOM node.

Because it actually is a DOM node. Well, technically it’s a reference to a DOM node, but console represents it as DOM node for convenience.

And this is the reason why memory footprint keeps growing. Garbage collector can not remove those canvases from memory, because there are references to every canvas resized by THREE.js to match POT rule.

Even when console is closed

You might think that this issue won’t impact real users, because they usually don’t have console opened.

But actually, logs are preserved even when console is closed. That’s why you can see logs when you open console after they were logged (which is very handy by the way).

Solution

The best solution is to avoid NPOT textures. This way you’ll avoid memory leaks caused by console.warn. But you’ll also get better performance, because THREE.js will not resize your textures to make them POT.

If for some reason you can’t use POT textures right now — that’s alright, there already is approved pull request in THREE.js repo, which removes DOM nodes references from console warnings. And, hopefully, next release will contain those changes.

UPDATE: PR mentioned above was merged and released in THREE.js v95

If you don’t want to wait for next release or you’re using older version of THREE.js and can’t upgrade easily (like me) — here’s a code snippet:

// See https://github.com/mrdoob/three.js/pull/14483
const consoleWarn = window.console.warn;
window.console.warn = function() {
  const string = arguments[0];
  const filterStrings = [
    'is not power of two',
    'image is too big',
    'marked for update',
  ];
  // filter warnings containing above strings
  const shouldBeFiltered = filterStrings.some(string => (
    arguments[0].includes('THREE') && arguments[0].includes(string)
  );
  if (shouldBeFiltered) {
    // log it to console without second argument, which contains reference to `image`
    consoleWarn.call(null, arguments[0]);
  } else {
    // pass other warnings without changes
    consoleWarn.apply(null, arguments);
  }
};

Insert that code before your app initialization. It will take all THREE.js console warnings (not only NPOT ones) that pass texture references to console, and log them without those references. All other warning in your app will remain untouched.

Summary

This article is based on my personal optimization experience. But same techniques may apply to most JavaScript applications.

The conclusion is that logging large objects or DOM references to console might not be a good decision, so be careful. Also, good option would be turning logs off in production.

Written by Andrew Cherniavskii, Software Engineer.