three.js frame drops: shader compiles, uploads and GC
The classic three.js stutter goes like this. The scene runs smoothly until something new appears: a new material, a new light, an effect, a model streaming in. Then there's a freeze of tens or hundreds of milliseconds, and it's smooth again.
That freeze is almost always one of three things: a shader compiling, a texture uploading to the GPU, or garbage collection. Myaku tells you which, frame by frame.
Add it to your scene
npm install -D myaku// main.ts, the first line
import "myaku/auto";
import * as THREE from "three";Myaku listens on three.js's own devtools hook (__THREE_DEVTOOLS__), so when you create a WebGLRenderer, myaku finds it and times every renderer.render() call as a render span. It also wraps the WebGL context, so it counts what three.js sends to the GPU each frame: draw calls, texture and buffer uploads in bytes, and shader compiles.
Load it before you create the renderer, so it sees the context. If you don't, myaku warns you that it saw no canvas context, instead of quietly reporting nothing.
Shader compiles
three.js compiles a program the first time it draws a material and lighting combination it hasn't seen. On a slow GPU driver, that can take far longer than a frame. Myaku counts compileShader and linkProgram calls per frame, so the verdict reads:
Frame 2,031 took 140 ms (budget 25 ms). 2 shader compiles or program links happened during play. Compile shaders while loading (warm up materials and effects before the level starts), not the first time they're used.
The fix: compile ahead of time with renderer.compile(scene, camera) (or compileAsync) once everything is in the scene, before the player can see it.
Texture uploads
Loading a texture doesn't upload it. three.js uploads it on the first frame that uses it. A 4K texture is 64 MB on the GPU, and that upload happens inside a frame. Myaku counts the bytes per frame:
Frame 3,377 took 58 ms (budget 25 ms). 3 uploads sent 48 MB to the GPU this frame.
The fix: renderer.initTexture(texture) during loading, smaller or compressed textures (KTX2 with Basis), and staggering the reveal of new models.
Garbage collection
new THREE.Vector3() in the render loop, clone() every frame, and arrays rebuilt per frame all add up to collections. Myaku sees the heap shrink by megabytes while your code was idle, and calls it gc. Reuse vectors and matrices declared outside the loop.
Myaku checks that the browser reports the heap finely enough to see this. If it doesn't, myaku says GC can't be ruled out rather than guessing.
Your own systems
render tells you how long three.js spent drawing. For everything else, add spans:
import { myaku } from "myaku";
const profiler = myaku();
renderer.setAnimationLoop(() => {
profiler.span("physics", () => world.step(1 / 60));
profiler.span("animation", () => mixer.update(clock.getDelta()));
renderer.render(scene, camera); // timed as "render" automatically
});What myaku can't see
Myaku doesn't time the GPU itself yet. If your code is quick and nothing else explains a slow frame, the verdict is unknown with low confidence. When draw calls run into the thousands, it adds a hint that the GPU may be the bottleneck. It says what it doesn't know rather than guessing.
Get started with Myaku → · Spans and engines · Reading verdicts