Command Palette

Search for a command to run...

Hectal
Course outline
Topic 17.4Advanced ~22 min · step 4 of 7

Memory, Disposal and Leaks

In one line

three.js uploads geometries, textures and shaders to the GPU and never frees them by itself. dispose() frees them, but only the one object you call it on. A configurator that makes a texture per click leaks until the tab dies.

0/7 · 0%

Think of it like this

A rented storage unit. Putting a box in costs a little. Throwing the box away at home does not empty the unit. You must go and clear it, and the rent runs until you do.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

dispose()
Frees an object's GPU resources (buffers, textures, programs). The JavaScript object stays usable and is uploaded again if drawn.
leak
Memory that is no longer needed but is never freed, so use only grows.
render target
A texture the GPU draws into (for effects or mirrors). It owns GPU textures that you must dispose.
shader program
The compiled GPU code for a material. Materials of the same kind can share one.
mipmaps
Smaller copies of a texture for distant surfaces; they add about a third to its memory.

Step by step

01Watch the counters fall

Run the first example. Removing meshes changes nothing. Each dispose() frees one kind.

02Repeat the configurator bug

The second example is the bug and two fixes, with the texture count after 1, 5 and 20 colour changes.

03Dispose a whole tree

The third example frees a group with shared textures and a render target; material.dispose() alone leaves textures behind.

04Make it a habit

The part that made a resource owns it: dispose it where that part is removed. Check renderer.info.memory before and after a change.

Try it yourself

  1. 1

    Skip a step

    In the first example, delete the line for (const t of things) t.material.map.dispose();. The last two lines then print textures 10: the ten pictures are still on the GPU, and "one thing drawn again" shows geometries 1, textures 10, programs 1.

  2. 2

    Leak ten times more

    In the second example, change i <= 20 to i <= 200 and i === 20 to i === 200. The leaky run prints after 200: +200 (about 68 MB at 341 KB each), and the other two print after 200: +1.

Code & diagrams

What dispose() frees, one step at a time New tab

Ten 1 x 1 pixel textures keep the counts easy to read. The pattern is the same for 2048 x 2048 ones.

idle

Console

Run the scene to see its console.

Expected console output

start                            geometries 0, textures 0, programs 0
10 things drawn once             geometries 10, textures 10, programs 1
removed from the scene           geometries 10, textures 10, programs 1
geometry.dispose() on all        geometries 0, textures 10, programs 1
material.dispose() on all        geometries 0, textures 10, programs 0
texture.dispose() on all         geometries 0, textures 0, programs 0
one thing drawn again            geometries 1, textures 1, programs 1
A configurator that leaks a texture per click, then fixed New tab

"extra textures" counts textures above the number when each run began.

idle

Console

Run the scene to see its console.

Expected console output

leaky        extra textures after 1: +1 | after 5: +5 | after 20: +20
dispose old  extra textures after 1: +1 | after 5: +1 | after 20: +1
reuse one    extra textures after 1: +1 | after 5: +1 | after 20: +1
256 x 256 RGBA with mipmaps is about 341 KB each: 20 leaked colours cost about 6.7 MB
disposeTree: shared textures, and a render target New tab

Every printed number is renderer.info after the step. The depth texture of the target counts as a texture. The one texture left at the end is three.js's own lookup table for physical materials.

idle

Console

Run the scene to see its console.

Expected console output

3 meshes, 2 shared textures      geometries 3 | textures 3 | programs 1
+ a render target drawn into     geometries 3 | textures 5 | programs 2
target.dispose()                 geometries 3 | textures 3 | programs 2
one material.dispose()           geometries 3 | textures 3 | programs 2
disposeTree freed 2 distinct textures
disposeTree(body)                geometries 0 | textures 1 | programs 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to dispose the old texture

In the second example, delete the line if (old) old.dispose(); from fixedChange.

terminal
$ Run
── what you'll see ──
dispose old extra textures after 1: +1 | after 5: +5 | after 20: +20
The "fixed" run now leaks exactly like the first one.

Myth vs fact

Myth

Removing a mesh from the scene frees its memory.

Fact

It only stops it being drawn. The geometry, textures and programs stay on the GPU until you dispose them (or the page closes).

Myth

material.dispose() frees the material's textures.

Fact

It frees the shader program only. Textures are separate objects and must be disposed one by one (and once, if shared).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    renderer.dispose() frees the renderer's own internal resources but does not lose the context or remove the canvas. A page that creates and drops renderers (a single-page app between routes) should call it, and can also call renderer.forceContextLoss() so the browser's 16-context limit is not reached.

Remember this

  1. 1

    The problem this solves: JavaScript has a garbage collector, so people expect memory to look after itself. GPU memory is not garbage-collected. Removing a mesh from the scene frees nothing on the GPU. A page that adds and drops objects slowly eats the GPU's memory, then loses its context or crashes on a phone.

  2. 2

    What lives on the GPU. Geometry attributes, textures, render targets and compiled shader programs. renderer.info.memory.geometries and .textures count the first two, and renderer.info.programs.length the last. They rise when something is first drawn and fall only when you dispose.

  3. 3

    **What dispose() does.** geometry.dispose() frees its buffers. material.dispose() lets the renderer drop its shader program if no other material uses it. texture.dispose() frees the picture. renderTarget.dispose() frees its textures (and depth buffer). The first example shows each count fall: geometries to 0 after the geometry calls, programs to 0 after the materials, textures to 0 after the textures.

  4. 4

    What it does not do. material.dispose() does not dispose the textures inside the material (the second step of the example leaves 10 textures). mesh.removeFromParent() disposes nothing. And disposing is not deleting: draw the object again and three.js uploads it again (the last line of the first example: 1 geometry, 1 texture).

  5. 5

    The leak. A colour configurator makes a new texture for each colour and sets it on the material. The old texture is no longer used, but it stays on the GPU. The second example does 20 colour changes three ways: new texture each time (+20 textures: the leak), dispose the old one first (+1), or repaint one texture and set needsUpdate (+1). A 256 x 256 texture with mipmaps costs about 341 KB on the GPU, whatever its file size (Topic 6.6), so 20 leaks cost about 6.7 MB. A 2048 x 2048 texture is 21 MiB each, and a 4096 x 4096 one 85 MiB: a few of those in one "huge" model can be more than a cheap phone gives a tab, and then the page reloads or loses its context (Topic 18.2).

  6. 6

    A helper for whole trees. The third example's disposeTree(root) visits every mesh, disposes its geometry, its material(s) and each texture inside the materials, once. Textures shared by three meshes are freed once. Use it when a part is gone for good, not on something you will show again soon.

Explain it without notes

01

A configurator's memory grows by 340 KB at every colour click. Find the cause and give two fixes.

02

Why must you dispose a material's textures separately?

Practice

01

Write swapMap(material, newTexture) that never leaks, and does not free a texture that is the same as the new one.

02

You remove a mesh with scene.remove(mesh) and renderer.info.memory.textures does not change. Why, and what do you add?

Done when you can

  • Read geometries, textures and programs from renderer.info

  • Say what each dispose() frees and what it leaves

  • Reproduce and fix the texture-per-click leak

  • Write and use a disposeTree helper