Docs·Charting Library·Guides

Performance & Smoothness

Patterns that keep the chart at 60 fps during pan, zoom, replay, and high-frequency data ingest. The do/don't list for trading-grade smoothness.

The engine paints at native speed — most performance complaints we see come from how data is fed in, not from the renderer itself. This page lists the patterns that keep things smooth, the anti-patterns that don't, and the exact symptoms each one causes so you can diagnose your own UI in under a minute.

The mental model

Two clocks drive the chart:

  1. Data clock — your websocket / REST stream delivering ticks, depth snapshots, trades.
  2. Render clock — the browser's vsync, ~16.6 ms on a 60 Hz display.

Smoothness happens when the first clock never blocks the second. Everything below is a consequence of that.

✓ Things to do

Batch your tick feed

Exchange websockets often deliver 50-200 messages per second. If you call updateLastKline 200 times in 16 ms, you're paying the cost 200× per frame. Coalesce on your side:

JavaScript
let pending = null
let raf = 0

ws.onmessage = (e) => {
  pending = mergeTick(pending, JSON.parse(e.data))
  if (raf) return
  raf = requestAnimationFrame(() => {
    chart.updateLastKline(
      pending.ts, pending.o, pending.h, pending.l, pending.c, pending.v,
    )
    pending = null
    raf = 0
  })
}

Effective result: at most one chart call per frame, no matter how chatty the feed.

Use typed arrays for bulk loads

setKlines(t, o, h, l, c, v) accepts Float64Array or number[]. Typed arrays skip a copy step. On a 50 000-bar initial load that's the difference between an imperceptible 4 ms and a visible 40 ms hiccup.

JavaScript
const t = new Float64Array(rows.length)
const o = new Float64Array(rows.length)
// ... fill in one tight loop, then:
chart.setKlines(t, o, h, l, c, v)

Call prefetchEngine() at app boot

The engine module is fetched and compiled on demand. Calling prefetchEngine() as early as your app can — typically in main.ts or a top-level <script setup> — moves that cost off your chart's mount frame.

JavaScript
import { prefetchEngine } from '@mrd/chart-engine'
prefetchEngine()  // fire-and-forget

No await, no error handling — it's idempotent and gracefully no-ops on a slow network.

Match the canvas resolution to the device

Pass the canvas element directly, let createChartBridge measure the device pixel ratio once, and call chart.resize() only when the parent element's size actually changes — not on every window.resize event.

JavaScript
const ro = new ResizeObserver(() => chart.resize())
ro.observe(canvas.parentElement)
// Cleanup on unmount:
onBeforeUnmount(() => { ro.disconnect(); chart.destroy() })

ResizeObserver fires only on real layout changes; window.resize fires once per orientation change AND once per scrollbar appearance AND once per zoom change. Use the former.

Pause when the tab is hidden

The browser already throttles requestAnimationFrame in background tabs, but indicator math and JSON parsing don't pause automatically. Hook into visibilitychange:

JavaScript
document.addEventListener('visibilitychange', () => {
  if (document.hidden) chart.pause()
  else                 chart.resume()
})

On a long-lived trading dashboard this is the single biggest battery / CPU win — pausing a backgrounded chart drops its main-thread work to zero.

Destroy on unmount, every time

Mounting a chart inside a route-component without calling destroy() on unmount leaks: the render loop keeps running, event listeners stay attached, the canvas keeps painting at 60 fps invisibly. Three route changes in and the tab uses 60 % CPU for "nothing".

JavaScript
// Vue
onBeforeUnmount(() => chart.destroy())

// React
useEffect(() => () => chart.destroy(), [])

destroy() releases everything — render loop, listeners, internal buffers. The chart instance is unusable after destroy(); create a new one if you need to mount again.

Send sorted, deduped data

If your data source can emit duplicate or out-of-order timestamps (some exchange websockets do during reconnects), dedupe in your handler before calling appendKline. The engine assumes timestamps are strictly increasing; feeding it duplicates produces undefined rendering.

✗ Things to avoid

Don't call setKlines on every tick

setKlines is the full-reload method — it resets the dataset, the viewport, and the cursor state. On a 30 000-bar dataset it does real work. For tick updates use updateLastKline; for closed bars use appendKline.

Symptom: viewport snaps back to "latest 120 candles" on every tick, can't pan back.

Don't JSON.parse the tooltip payload inside a framework render

Tooltip fires up to 60× / sec. If your callback triggers a React setState or Vue ref write that re-renders an ancestor component, you're paying a full reconcile per frame.

JavaScript
// ❌ Bad — every hover frame triggers a Vue/React reconcile
chart.onTooltip((raw) => { tooltipData.value = JSON.parse(raw) })

// ✓ Good — write directly into DOM, framework never knows
chart.onTooltip((raw, sx, sy) => {
  if (!raw) { tipEl.style.display = 'none'; return }
  const p = JSON.parse(raw)
  tipEl.textContent = `${p.kline.close.toFixed(2)}`
  tipEl.style.cssText += `;display:block;left:${sx + 12}px;top:${sy + 12}px`
})

Symptom: smooth chart, janky hover. CPU profile shows your component's render function dominating.

Don't run chart.resize() in a tight loop

resize() recomputes layout, regenerates internal buffers, and forces a repaint. Calling it on window.resize (which fires ~10× during a drag-resize) costs frames.

Symptom: visible flicker / freeze while the user resizes the browser. Use ResizeObserver instead, debounced if the parent itself is animating.

Don't append depth columns slower than the depth-snapshot rate

The orderbook heatmap holds one column per snapshot in a ring buffer. If your snapshots arrive at 10 Hz but you only call appendHeatmapColumn at 1 Hz, you've thrown away 90 % of the depth resolution and the heatmap looks chunky.

If your data clock is slower than 10 Hz, that's fine — the heatmap was designed for fast feeds. Just don't artificially throttle on your side.

Don't repeatedly toggle indicators in user code

enableRsi(true) / enableRsi(false) allocates and frees the indicator's series buffer. Toggling at 60 Hz to "animate" the indicator on/off thrashes allocations. If you need a fade, fade the canvas's opacity in your CSS layer — the indicator itself stays on.

Don't compute drawings from inside an animation loop

addTrendline / addHorizontal are persistent — they live in the engine until you remove them. Calling addTrendline on every frame stacks N drawings per second.

Symptom: chart slows over time, scrolling reveals hundreds of overlapping lines.

Don't init two charts on one canvas

createChartBridge(canvas) takes ownership of the canvas (event listeners, transform matrix, the works). Initialising twice without destroying the first instance produces two render loops fighting over the same pixels.

Symptom: hover events fire twice, drawings appear at wrong positions, frame timing oscillates.

Don't keep the parent dialog mounted while hidden

A common React / Vue pattern is to render the chart inside a <Modal> and toggle the modal's display: none instead of unmounting. The chart's render loop keeps running behind the modal, wasting CPU.

Either unmount the chart when the modal closes (clean), or call chart.pause() while hidden and chart.resume() when shown (lazy).

Measuring it

Chrome DevTools → Performance → record 5 seconds of chart pan. The frames pane should show consistent 16-ms bars with no red frames. If you see red:

  • Long yellow (Scripting) blocks → your data feed is too chatty, batch as shown above.
  • Long purple (Rendering) blocks → too many simultaneous indicators or drawings, prune what's not needed.
  • Long green (Painting) blocks → the canvas is too large, lower the device pixel ratio if your display is over 2×.

Next

  • Common Pitfalls — the catalogue of bugs we see most often in production support tickets, with the one-line fix for each.
  • Chart Instancestart, stop, pause, resume, destroy — the lifecycle methods used in every recipe above.
  • Events & Tooltips — tooltip throttling done right.