Course outline
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.
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
Skip a step
In the first example, delete the line
for (const t of things) t.material.map.dispose();. The last two lines then printtextures 10: the ten pictures are still on the GPU, and "one thing drawn again" showsgeometries 1, textures 10, programs 1. - 2
Leak ten times more
In the second example, change
i <= 20toi <= 200andi === 20toi === 200. The leaky run printsafter 200: +200(about 68 MB at 341 KB each), and the other two printafter 200: +1.
Code & diagrams
Ten 1 x 1 pixel textures keep the counts easy to read. The pattern is the same for 2048 x 2048 ones.
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"extra textures" counts textures above the number when each run began.
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 MBEvery 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.
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 0Break 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.
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 callrenderer.forceContextLoss()so the browser's 16-context limit is not reached.
Remember this
- 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
What lives on the GPU. Geometry attributes, textures, render targets and compiled shader programs.
renderer.info.memory.geometriesand.texturescount the first two, andrenderer.info.programs.lengththe last. They rise when something is first drawn and fall only when you dispose. - 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
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
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
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
A configurator's memory grows by 340 KB at every colour click. Find the cause and give two fixes.
Why must you dispose a material's textures separately?
Practice
Write swapMap(material, newTexture) that never leaks, and does not free a texture that is the same as the new one.
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.infoSay what each
dispose()frees and what it leavesReproduce and fix the texture-per-click leak
Write and use a
disposeTreehelper