How to make performant ThreeJS HUD

Published on Β· Edit on Github Β· llms.txt

#threlte#svelte#performance#3d#uikit

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:

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>
		

Comments

Socials

Links

Miscellaneous

  1. [1] All opinions are my own, except those generated by large language models.
  2. [2] Fonts: ...
Guybrush.ink
Made with β™₯ in Paris, London & Toulouse build: main