How to make performant ThreeJS HUD
Published on Β· Edit on Github Β· llms.txt
When I started sprinkling status panels on top of my 3D robots in RobotHub I hit an unexpected wall: frame-rates nosedived each time I added a new HTML overlay. This post is a quick note on why that happens and how I fixed it with
threlte-uikit
.
The hidden price of <Html>
<Html>
(alias <HTML>
in Threlte) works by creating an absolutely-positioned DOM element that is re-projected each frame so it appears to stick to a 3D object. That sounds cheap, yet browsers must:
- recompute layout & style for every element when the camera moves (every frame at 60 Hz)
- perform an expensive GPU compositing pass β mixing the WebGL canvas with the DOM layer
- keep two coordinate systems in sync (CSS pixels βοΈ world units)
- bridge pointer / focus events between WebGL and the DOM tree
Community war-stories echo this:
- "20
<Html>
panels dropped Chrome from 120 fps to 50 fps" β pmndrs discussion link - Discourse thread on low FPS with many HTML overlays link
Rule of thumb from R3F performance guide: "If you can, keep UI inside the scene graph. Each DOM node is a liability." link
flowchartLR subgraphDOM_Overlay[DOMOverlayRoute] A1(CameraMove)-->B1[Update<Html>position] B1-->C1[Reflow/Repaint] C1-->D1[CompositeCanvas+DOM] end subgraphUIKitPath[UIKitRoute] A2(CameraMove)-->B2[ModelViewMatrix] B2-->C2[SingleDrawCall] end styleDOM_Overlayfill:#fef3c7,stroke:#f59e0b styleUIKitPathfill:#dcfce7,stroke:#16a34a
Why it gets worse at scale
Adding ten robots in RobotHub meant 50+ status widgets (3 status widgets per robot) β¦ Each widget held ~15 nested <div>
s. At 60 fps that is 45 000 layouts / s before we even start rendering meshes.
You can try it out yourself:
- Old UI with HTML Status Widgets
Old UI with HTML Status Widgets - New UI with threlte-uikit Status Widgets
New UI with threlte-uikit Status Widgets
Meet threlte-uikit
threlte-uikit ports pmndrs' WebGL-native UIKit to Threlte. Widgets are instanced quads rendered in the same pass as your meshesβno DOM, no compositor juggling.
src/components/HUD.svelte
<scriptlang="ts"> import{Root,Container,Text,Image,SVG,Video,Fullscreen}from'threlte-uikit'; </script> //UseinaThrelteContext(insidea<Canvas>) <Rootpadding={12}backgroundColor="#111827"> <Containerdirection="row"gap={8}align="center"> <Imagesrc="/icons/gpu.svg"width={24}height={24}/> <Texttext="RTX-A4000"size={14}/> <SVGsrc="/icons/thermometer.svg"width={12}/> <Texttext="66Β°C"color="#f87171"size={12}/> </Container> </Root>