Skip to main content
Field Notes/Engineering
Engineering8 MIN READ · MAY 14, 2026

Why our cardiac monitor renders at 60fps in the browser

A walk-through of the rendering pipeline: requestAnimationFrame-driven SVG sweeps, clipPath caveats, and why we ditched canvas for the strip but kept it for capnography. Plus the one Safari bug that ate three weekends.

JR
Jordan Reyes
Lead engineer · Paramedic

When we set out to build the SimuPro cardiac monitor, we had a non-negotiable: it must look and feel like the real thing on a Zoll, a LIFEPAK, or a Philips MRx — and it must do that in a browser tab, on a Chromebook, in a station crew room, mid-shift. Here's how we got there, where we burned weekends, and what we'd do differently.

The starting problem

A live ECG strip looks deceptively simple. Three boxes wide, twenty-five millimeters per second, a sawtooth grid, and a glowing trace that wraps every ~6 seconds. Easy, right? Three weekends in, we had: a janky sweep that dropped frames on every state update, a clipPath that visibly clipped at the wrong subpixel on Firefox, and a capnograph that lagged the ECG by ~80ms on slower machines.

We had a choice: <canvas> with manual draw calls, or <svg>with strategic mutation. We picked SVG, and that's most of what this post is about.

Why not canvas?

The instinct is canvas — it's designed for high-FPS pixel work. But for a clinical monitor specifically, three properties of SVG turned out to matter more than raw throughput.

Hit-testing for free

Learners can tap any waveform to bring up its alarm config. With canvas, that's a manual implementation: track every drawn segment, intersect with pointer events. With SVG, you get it as DOM pointer-events. We never wrote a hit-test routine. One weekend saved, instantly.

Subpixel crispness

An ECG trace at 1px stroke width is a brutal rendering target. Browsers gamma-correct strokes inconsistently between canvas and SVG. SVG, despite its quirks, gives us repeatable subpixel positioning when we use shape-rendering="geometricPrecision" and round our path commands.

// NOTE
If you're building an in-house monitor for a single hardware target, canvas is probably fine. We needed cross-browser parity from a Chromebook in a rural ambulance to a 5K iMac at a training center, and SVG won that bake-off by a margin we didn't expect.

The rAF sweep loop

Our sweep is one requestAnimationFrameloop per channel. Inside the loop, we don't re-render React — we mutate the x attribute of an SVG <g> directly. React knows nothing about the sweep, which means our component tree stays still.

function sweep(ts: number) {
  const dt = ts - lastTs;
  lastTs = ts;
  const pxPerMs = sweepSpeedPx / 1000;
  cursorRef.current += dt * pxPerMs;
  // mutate, don't setState
  groupRef.current!.setAttribute(
    'transform',
    `translate(${-cursorRef.current % tileWidth}, 0)`,
  );
  rafIdRef.current = requestAnimationFrame(sweep);
}
// WIN · Measure twice, ship once
We almostshipped a version that did setState on every tick — it "felt" fine in dev but pinned the CPU on production builds because of React's reconciliation cost. Always profile against a production bundle.

clipPath caveats

The strip wraps every ~6 seconds. That wrap is a <clipPath> — the trace renders into an infinitely long path, but only the visible window draws. Easy in theory; in practice, the clip-region cached at definition time on some browsers.

Workaround: never animate the clipPath. Instead, give it a fixed viewport, and translate the contents underneath it.

“Animate the contents, not the window.” — eight words that saved us a week.

The Safari bug that ate three weekends

Safari was rendering our ECG paths at the wrong subpixel offset after a tab returned from background. Couldn't reproduce in Chrome. Couldn't reproduce in Firefox.

Root cause: Safari was caching transform matrices on offscreen elements differently. The fix was to set the transform on a wrapper that was never offscreen.

// CAUTION · What we learned the hard way
Browser bugs in rendering pipelines aren't always reproducible in your dev environment. Build a staging environment that mirrors your weirdest production targets — including device wake/sleep cycles.

Takeaways

  • For interactive, clinical-feeling SVG strips: stay in SVG, mutate the DOM directly, keep React out of the inner loop.
  • Use canvas for fills, gradients, and decorative work where you don't need event handling.
  • Never animate a clipPath — animate the contents underneath it.
  • Profile against production. Always.
  • Test sleep/wake cycles on every browser you ship to. Especially Safari.

The cardiac monitor is the centerpiece of the SimuPro cockpit. If you want to put hands on it, the free tier includes the full ECG monitor in every scenario — no card required.