React and Three.js for Real-Time Market Visualization
The frame budget is won at one seam: where React stops reconciling and Three.js starts drawing.
§ IFrame
The Ops lesson today set a contract: sixty frames a second through five hundred ticks. That contract is not met in a build config or a dashboard. It is met or missed in two pieces of code — the place where React decides what changed, and the place where Three.js draws what changed. Everything else is measurement. This is the work.
React and Three.js solve the same problem from opposite ends. React owns the document: it takes your description of what the screen should show and computes the smallest set of changes to the actual DOM that makes the screen match. Three.js owns the canvas: it takes a scene of objects and draws them to the GPU, frame after frame, on a loop you control. A market visualization needs both. The order book and the controls are React's job. The live depth surface, the animated price ribbon, the ten-thousand-point heatmap are Three.js's job, because the DOM cannot draw ten thousand changing things sixty times a second and the GPU can.
The danger is the seam between them. React wants to re-render on state change. Three.js wants to render on the animation frame. Wire them together carelessly and React's render cycle fights the GPU's render loop, every tick triggers a reconciliation of a tree that should never have re-rendered, and the frame budget is gone before a single pixel is drawn. This is the first Sunday lesson under the framework-pair rotation, and it opens the track on exactly this seam.
§ IILanguage Idiom — Two Render Models, One Frame
React's model: reconciliation, and the boundary that stops it
When state changes, React re-runs the component and builds a new tree of what the UI should be. It then compares that tree against the previous one and applies only the differences to the DOM. The comparison is reconciliation, and the internal structure that holds the work is the Fiber tree (Learning React, Ch 9, pp. 234–235). Reconciliation is cheap when the tree is small and expensive when it is large, and the thing that makes a tree expensive is re-rendering a parent that did not need to re-render, because every child re-renders with it by default.
The idiom that stops the cascade is the memoization boundary. React.memo wraps a component so it re-renders only when its props actually change by reference. useMemo caches a computed value so it is recomputed only when its dependencies change. useCallback caches a function so its reference stays stable across renders, which is what lets a memo-wrapped child believe its props are unchanged. These three are the reconciliation boundary: they draw a line in the tree past which a state change does not propagate.
Three.js's model: the render loop, and the work you keep out of it
Three.js does not re-render on state change. It renders when you tell it to, and for a live scene you tell it to on every animation frame through requestAnimationFrame. The browser calls your loop function once per frame, ideally every 16.7 milliseconds, and your job inside that call is to update the scene's objects and issue the draw. The expensive part is rarely the draw itself; it is the per-frame work in JavaScript before the draw — recomputing positions, rebuilding geometry, allocating objects the garbage collector will later reclaim mid-frame.
The idiom that protects the loop is to do the allocation and the heavy computation once, outside the loop, and mutate in place inside it. Create the geometry buffers at setup. Inside the frame, write new values into the existing buffers and mark them for upload, rather than building new ones. And when the scene has many similar objects (every level of a depth book is a similar bar), draw them with instancing: one GPU submission for all ten thousand, instead of ten thousand draw calls.
§ IIICode Worked Example — A Live Depth Surface Inside a React Page
Build the seam correctly. The React side first. The component mounts a canvas and hands its element to a Three.js controller through a ref. The ref is the bridge; it carries no value React renders.
function DepthSurface({ instrument }) {
const canvasRef = useRef(null);
const controllerRef = useRef(null);
useEffect(() => {
const controller = new DepthController(canvasRef.current);
controllerRef.current = controller;
controller.start();
return () => controller.dispose();
}, []);
useEffect(() => {
controllerRef.current?.setInstrument(instrument);
}, [instrument]);
return <canvas ref={canvasRef} className="depth-surface" />;
}
export default React.memo(DepthSurface);
Read what this does and does not do. The first effect runs once on mount: it constructs the controller, starts its render loop, and registers cleanup that disposes GPU resources on unmount. The second effect runs only when instrument changes, handing the new instrument to the controller imperatively. The component is wrapped in React.memo, so a parent re-render does not re-render it unless instrument changed by reference. Critically, no market tick is anywhere in this component. Ticks never touch React.
The ticks go straight to the controller, which holds the render loop. Here the discipline is allocate-once, mutate-in-frame.
class DepthController {
constructor(canvas) {
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
this.scene = new THREE.Scene();
this.camera = makeOrthoCamera();
this.levels = new THREE.InstancedMesh(barGeometry, barMaterial, MAX_LEVELS);
this.scene.add(this.levels);
this.pending = null;
this.running = false;
}
onTick(book) {
this.pending = book;
}
start() {
this.running = true;
const loop = () => {
if (!this.running) return;
if (this.pending) {
writeLevelsToInstances(this.levels, this.pending);
this.pending = null;
}
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
dispose() {
this.running = false;
this.levels.geometry.dispose();
this.renderer.dispose();
}
}
Walk the loop. The instanced mesh is allocated once in the constructor with capacity for the maximum number of levels; no per-frame allocation happens. Ticks arrive through onTick, which does the cheapest possible thing: it stores the latest book in pending and returns. It does not draw. When four hundred ticks arrive in a second, four hundred calls overwrite pending, and the loop reads it once. That is coalescing: the screen shows the latest state, not every intermediate one, exactly the discipline the Ops lesson named. Inside the frame, if a book is pending, the controller writes its values into the existing instance buffers and issues one render. One draw call paints every level because the mesh is instanced.
The disposal path matters as much as the loop. A long-lived trading session that mounts and unmounts depth surfaces as the operator switches views will leak GPU memory until the tab dies if dispose does not run. React's cleanup return calls it; Three.js's dispose frees the geometry and the renderer's GPU allocations. Forget this and the frame budget is fine for an hour and gone by lunch. The useRef bridge and the effect dependency arrays are the React idioms doing their job (Learning React, hooks reference, pp. 306–307): useRef gives a mutable handle that survives renders without causing them.
§ IVConnection to Today's Ops Lesson
The Ops lesson set three numbers: bundle under budget, vitals as SLOs, sixty frames per second under five hundred ticks. This code is where the third number is won or lost. The coalescing in onTick is the frame-budget discipline the Ops lesson named, implemented: four hundred ticks become sixty renders because the loop reads the latest pending state once per frame and the intermediate states are dropped before they ever reach the GPU. The instanced mesh is how ten thousand depth levels fit inside one 16.7-millisecond draw window instead of blowing it across ten thousand draw calls. And the React.memo boundary is why a tick does not trigger a reconciliation of the whole page.
The Ops regression gate measures INP and frame rate; this is the code whose structure those measurements grade. When the gate flags an INP regression because a chart's handler ran on the main thread inside an interaction frame, the fix is structurally this lesson: get the per-tick work off the render path and onto a mutate-in-frame buffer, or off the main thread entirely. The Ops budget is the contract. The reconciliation boundary and the render loop are where the contract is signed.
§ VPrior-Lesson Reach
Streaming Market Data in the Browser (Sun 2026-05-24). That lesson built the pipe that delivers ticks to the browser. This lesson is what the browser does with each tick once it arrives: route it to a mutable controller, coalesce it, and draw the latest state on the frame. The pipe's far end is this render loop.
Instrumenting the Browser for Production Observability (Sun 2026-06-07). That lesson built the capture layer, the PerformanceObserver for long tasks. The long-task signal it captures is exactly the symptom this lesson's discipline prevents: a frame that overran because per-tick work ran on the main thread. That lesson sees the dropped frame; this one stops it from dropping.
Browser Trust Boundaries (Wed 2026-06-10). That lesson set Content Security Policy and Subresource Integrity. A Three.js app loading shaders and worker scripts lives inside that CSP; the worker-src and script-src directives govern where the off-main-thread computation this lesson recommends may load from. The render boundary and the trust boundary are both held by the same page at once.
§ VIClosing
React and Three.js meet at one seam, and the frame budget is won there or nowhere. React owns the document and stops its own reconciliation at the memoization boundary; Three.js owns the canvas and protects its render loop by allocating once and mutating in frame; the two talk through a mutable ref so the fast tick path never triggers a React render. Build the seam that way and ten thousand depth levels animate at sixty frames a second while the order book stays responsive to the human's hand. Build it carelessly and every tick re-renders the page and the GPU never gets its 16.7 milliseconds.
Open the live visualization you maintain. Find where a market tick enters it. Trace whether that tick triggers a React render. If it does, you are paying reconciliation cost sixty times a second for data that should have gone straight to a buffer. Move it off the React path first.
Paired Ops lesson → 01-Earth-DevOps/Synthesis-Lessons/2026-06-14-frontend-performance-budgets-...
Filed 2026-06-14 Sunday Fajr · First Sunday Web-Stack framework-pair lesson (React + Three.js, W1) under §II.D.4 rotation
Backward-Synergy-Reach → Streaming Market Data (Sun 05-24) · Browser Observability (Sun 06-07) · Browser Trust Boundaries (Wed 06-10)
HTML shipped in-cycle per HARD DISCIPLINE · per-framework section coloring React=water / Three.js=fire · grounded Learning React Ch 9 · Three.js-side knowledge-gap logged