I built LearnAnatomy3D (learnanatomy3d.com), a free 3D human anatomy atlas that runs in the browser:
- ~2,900 clickable structures across 11 body systems;
- male and female bodies;
- guided tours and quizzes;
- 14 languages, no sign-up.
This post covers the performance problems I hit while loading ~2.9 million triangles, and how I fixed them.
Click any structure to see its name, Latin term, origin, insertion and action:
The stack
- Next.js (App Router): the site, 14 locales, and the static anatomy and disease pages.
- three.js through react-three-fiber: the 3D viewer.
- Open anatomy datasets (Z-Anatomy, BodyParts3D, Human Reference Atlas), fitted to realistic character bodies with custom Node and Blender tooling, packed into gzip'd binary chunks and streamed in two passes: body surface first, full anatomy after.
Problem 1: the page froze for 7 seconds while loading
Clicks, navigation, even the "I understand" button: nothing responded while the model loaded. The Long Tasks API showed one 7.3-second task and about 13.6 s of blocking in total.
A CPU profile pointed at getProgramInfoLog. On a shader's first use, three.js reads its info log to report errors. Reading that log forces the browser to wait for compilation to finish, so KHR_parallel_shader_compile never gets the chance to help.
Fix 1: turn the check off in production, when the renderer is created. react-three-fiber renders the environment map before onCreated runs, so setting the flag there is too late.
<Canvas
gl={(defaults) => {
const renderer = new THREE.WebGLRenderer({ ...defaults, antialias: true });
renderer.debug.checkShaderErrors = process.env.NODE_ENV !== "production";
return renderer;
}}
/>
Fix 2: compile a model before it joins the scene. compileAsync polls the driver instead of blocking. The model is only attached once its shaders are ready, and the loading screen stays up until then.
await renderer.compileAsync(model.root, camera, scene); // still detached
setAttached(true); // now render it
Problem 2: textures decoded twice on the main thread
Each loading pass built its own materials, so every 2048² texture was decoded and uploaded twice, at about 150 ms each, on the main thread.
Fix: decode with ImageBitmapLoader, which works off the main thread, and cache by URL so both passes share one texture:
const cache = new Map<string, Promise<THREE.Texture>>();
const loader = new THREE.ImageBitmapLoader()
.setOptions({ imageOrientation: "flipY", premultiplyAlpha: "none" });
function surfaceTexture(url: string) {
if (!cache.has(url)) {
cache.set(url, loader.loadAsync(url).then((bitmap) => {
const t = new THREE.Texture(bitmap);
t.flipY = false; // already flipped by createImageBitmap
t.needsUpdate = true;
return t;
}));
}
return cache.get(url)!;
}
(Older Safari and Firefox ignore imageOrientation, so they fall back to TextureLoader.)
Problem 3: switching bodies rebuilt everything
Switching male ↔ female unmounted the <Canvas>. That threw away the WebGL context, every compiled program and the environment map. Fix: keep the canvas mounted. The old body stays under the loading screen until the new one is ready, so the click responds in about 0.1 s and compiled programs are reused.
Problem 4: 2,900 objects shouldn't mean 2,900 draw calls
Each body system is merged into one mesh per material. Every vertex carries a part index, and a small RGBA data texture holds the per-part state (visibility, highlight, opacity, explode offset). Selecting or hiding a structure is a texel write, never a re-merge.
Cross-sections use one persistent clipping plane per material, parked far away when the cut is off. Turning the cut on never changes the number of planes, so it never recompiles shaders.
Results
- Main-thread blocking during load: ~13.6 s → ~4 s.
- Longest freeze: 7.3 s → ~1.5 s. What's left is the GPU driver's first-ever shader compile.
- Body switch: the click responds in ~0.1 s.
What I'd love feedback on
How does it perform on your phone, and is anything confusing? Try it: learnanatomy3d.com. It's free, needs no account, and works in 14 languages.
Disclosure: I'm the developer of LearnAnatomy3D. The anatomy is real scientific data (Z-Anatomy, BodyParts3D, the Human Reference Atlas).





Top comments (1)
Great work! definitely runs smooth in the browser.