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