Guide · Frontend
Dashboard Frontend Design: Frame Budgets, Live Updates and Downsampling
32 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Dashboard frontend design is building the browser side of a live dashboard so many charts update without freezing the page. The core job is fitting each update into one frame, the picture a screen redraws about 60 times a second.
- At 60 frames a second a frame lasts 16.7 milliseconds (ms), and the Chrome team's guidance leaves about 10 ms for your code. So avoid drawing far more points than a chart has pixel columns.
- Downsampling keeps fewer points that still look right. LTTB (Largest-Triangle-Three-Buckets, from a 2013 thesis) keeps the line's shape, and min-max keeps each pixel column's lowest and highest value, so spikes survive.
- Let numbers arrive by push, through Server-Sent Events (a one-way stream from server to browser) or WebSockets (a two-way connection). Then touch the page at most once per frame with requestAnimationFrame, a browser call that runs code just before the next frame.
- Beginners can stop after the downsampling basics. Engineers then get results measured in Chromium 153 (the open-source core of Chrome): SVG (one element per shape) vs canvas (one painted bitmap) frame times. They also see hidden-tab timers fall to once a second, then once a minute, even with a WebSocket open.
What goes wrong when a live dashboard draws too much?
In dashboard frontend design, things go wrong when each new number triggers more drawing than one frame can hold. The page stutters, clicks lag, laptop fans spin up, and a tab left in the background comes back showing stale numbers.
Picture a common setup. An operations team at a food-delivery company in Bengaluru keeps one dashboard open all day, on laptops and on a wall TV. The setup is made up, but every timing below comes from our own runs or a cited source.
The dashboard has 12 line charts, such as orders per second and payment errors. Each shows the last 24 hours at one reading per second, which is 86,400 points per chart and 1,036,800 points on one screen.
It also has 6 KPI (key performance indicator) tiles, small boxes that each show one big live number. At peak the server pushes about 200 tile updates a second, and each chart is about 800 pixels wide.
Drawn naively, every chart tries to fit 108 points into each pixel column, and every update rewrites the page. This guide fixes that one budget at a time: time per frame, points per pixel, connections, background tabs, resizing and screen readers.
Which words do you need before designing a dashboard frontend?
Eight words carry the whole guide. Each one is tied to the operations dashboard.
- Frame: one picture the screen shows. A 60 Hz screen refreshes 60 times a second, so it shows a new frame every 16.7 ms if the page is ready in time.
- Main thread: the single line of work where the browser runs your JavaScript, works out styles and layout, and handles clicks. Long work here delays everything else.
- DOM: the Document Object Model, the browser's tree of page elements. Every heading, tile and SVG shape is a node in this tree.
- SVG and canvas: SVG (Scalable Vector Graphics) draws each line or dot as its own DOM element. A canvas is one element holding a bitmap, a grid of pixels your code paints.
- requestAnimationFrame: a browser function that runs your callback once, just before the next frame is drawn. Developers often shorten it to rAF.
- Downsampling: choosing a smaller set of points that still draws the same picture, such as 800 points instead of 86,400.
- Polling and push: polling means the browser asks the server for news on a timer. Push means the server sends news over a connection that stays open.
- Hidden tab: a tab the user cannot see, because another tab is in front or the window is minimised. The Page Visibility API tells your code when this changes.
What is dashboard frontend design and what must it get right?
Dashboard frontend design is the plan for how live numbers travel from the network to pixels without blowing any budget. It decides how data arrives, where it waits, when it is drawn and how much of it is drawn.
Think of four budgets. Time: each frame has 16.7 ms at 60 Hz. Pixels: a chart cannot show more detail than its width. Connections: browsers cap open connections per site. Attention: people using screen readers or keyboards need the same facts.
The design that respects all four is a small pipeline. Messages land in a store that keeps only what the screen needs. Each chart keeps a ring buffer, a fixed-size list that overwrites its oldest entry, so 24 hours of history never grows.
A scheduler wakes once per frame, downsamples each changed chart to its current width and draws it. Two watchers feed the scheduler: one for tab visibility and one for chart size.
The live dashboard pipeline
Drag to exploreNotice that messages and frames are decoupled. The stream can deliver 200 messages a second while the renderer draws at most once per frame, and only as many points as the chart has pixels.
Diagram connections:
- Metrics server to One SSE or WebSocket stream (push)
- One SSE or WebSocket stream to Latest-value store (every message)
- Latest-value store to Frame scheduler (book one frame)
- Frame scheduler to Downsampler (once per frame)
- Downsampler to Chart renderer (a few points per pixel)
- Chart renderer to Screen (paint)
- Visibility watcher to Frame scheduler (pause or resume)
- ResizeObserver to Downsampler (new width)
- Metrics server: Sends tile and chart updates
- One SSE or WebSocket stream: One connection carries every chart
- Latest-value store: Newest value per tile, ring buffer per chart
- Frame scheduler: One requestAnimationFrame per frame
- Downsampler: LTTB or min-max down to pixel width
- Chart renderer: Canvas for big series, SVG for small
- Screen: New frame every 16.7 ms at 60 Hz
- Visibility watcher: Pauses drawing while the tab is hidden
- ResizeObserver: Reports each chart's new width
An everyday picture helps with the hardest idea, keeping messages apart from frames. Think of the departure board at a railway station. Updates about trains arrive all the time, but the board is repainted only at set moments.
The board is the screen and each train's row is a tile. The repaint moment is the frame. The clerk who keeps only the newest status for each train is the latest-value store.
Where the picture breaks: a station board never stops, but a browser stops painting a hidden tab completely. And a chart needs a history of values, not only the newest one, which is why charts keep a ring buffer.
How much time does a dashboard get to draw one frame?
About 16.7 ms on a 60 Hz screen, and less on faster screens. Chrome's rendering guide on web.dev says browser housekeeping takes part of that, so your own work should finish inside about 10 ms.
In that time the browser runs your JavaScript, recalculates styles, works out layout, paints pixels and composites, meaning it stacks the painted layers into one image. If the total runs over, the frame is late and the screen shows the old picture again.
MDN, Mozilla's web developer reference, notes that requestAnimationFrame callbacks generally match the display's refresh rate. It lists 75, 120 and 144 Hz as widely used besides 60 Hz, and a 120 Hz laptop leaves half the time per frame.
| Screen refresh rate | Time per frame | Your share (about 60%) | Per chart if 12 charts redraw together |
|---|---|---|---|
| 60 Hz | 16.7 ms | 10.0 ms | 0.83 ms |
| 75 Hz | 13.3 ms | 8.0 ms | 0.67 ms |
| 120 Hz | 8.3 ms | 5.0 ms | 0.42 ms |
| 144 Hz | 6.9 ms | 4.2 ms | 0.35 ms |
For the operations dashboard, if all 12 charts redraw in the same frame, each gets well under 1 ms. That rules out walking 86,400 points per chart on every update.
To check your own page, record a trace in the browser's performance panel and look for frames marked as dropped or long. In code, compare the timestamps passed to consecutive requestAnimationFrame callbacks; a gap near 33 ms at 60 Hz means one frame was missed.
SVG vs canvas: which should a dashboard chart use?
Use SVG while a chart has about a thousand separate marks or fewer and needs easy styling, events and accessibility. Switch to canvas as marks head towards 10,000, or when the whole chart redraws every frame.
The reason is the DOM. In SVG every dot is an element the browser must style, lay out and keep in memory. A canvas is one element, so the cost moves into your drawing code and the painting of pixels.
We measured it with Playwright 1.63.0, a browser automation tool, and the build of Chromium 153 it bundles. Chromium is the open-source browser that Chrome is built on, and this build can differ from the Chrome on your laptop. A test page shifted the data every frame and redrew a full 800 by 300 pixel chart for 120 frames. We recorded the gap between frames, which includes style, layout and paint work done after our script.
| What was drawn | Marks | Median frame time | Late frames (of 119) | Median script time | DOM elements |
|---|---|---|---|---|---|
| SVG path (one line element) | 10,000 | 16.7 ms | 1 | 3.5 ms | 8 |
| Canvas line | 10,000 | 16.7 ms | 0 | 0.3 ms | 7 |
| SVG circles | 10,000 | 33.4 ms | 113 | 6.9 ms | 10,007 |
| Canvas circles (arcs) | 10,000 | 16.8 ms | 57 | 2.6 ms | 7 |
| Canvas squares | 10,000 | 16.7 ms | 0 | 1.5 ms | 7 |
| SVG path (one line element) | 50,000 | 33.2 ms | 60 | 19.8 ms | 8 |
| Canvas line | 50,000 | 16.7 ms | 2 | 1.1 ms | 7 |
| SVG circles | 50,000 | 183.3 ms | 119 | 31.9 ms | 50,007 |
| Canvas circles (arcs) | 50,000 | 150.0 ms | 119 | 14.7 ms | 7 |
| Canvas squares | 50,000 | 16.7 ms | 44 | 19.1 ms | 7 |
Pause and predict: at 1,000 marks, how many frames did each method miss? Every method missed at most one frame and held 60 frames a second. For small charts, pick SVG for its convenience.
What just happened at 10,000 marks? SVG circles missed 113 of 119 frames. Our script took only 6.9 ms, so most of each 33 ms frame went to work after it, such as style, layout and paint. Canvas squares missed none.
Two surprises are worth keeping. Canvas is not automatically fast: 10,000 round arcs missed 57 frames while 10,000 squares missed none. And one SVG path is cheap, because it is one element, until its point list grows to 50,000 points.
That 50,000-point SVG path varied most between runs, missing 60 and 90 of 119 frames in our two saved runs. At 50,000 round dots neither SVG nor canvas came near 60 frames a second, which makes the case for downsampling.
Canvas brings its own chores. MDN's canvas performance guide suggests batching drawing calls, avoiding needless state changes and turning off transparency with the alpha option when you do not need it. It also suggests layering canvases so static axes are not redrawn.
For heavy charts, OffscreenCanvas lets a Web Worker, a background thread, draw without blocking the main thread. MDN marks it Baseline widely available, meaning it has worked across major browsers since March 2023. We did not measure WebGL, the drawing interface that uses the graphics card and that some chart libraries use for millions of points.
Polling vs WebSockets vs Server-Sent Events: how should live numbers arrive?
For a dashboard that mostly receives numbers, Server-Sent Events (SSE) is the simplest push channel. Use WebSockets when the browser also sends a lot back. Plain polling suits slow dashboards; our rule of thumb is updates every 30 seconds or slower.
SSE is a long-lived HTTP response that the browser reads with the EventSource interface. The HTML standard gives it automatic reconnection, a retry field that sets the reconnection delay, and a Last-Event-ID header so the server can resend what was missed.
A WebSocket, defined in 2011 in RFC 6455, an internet standards document, carries two-way traffic over a single TCP connection, the reliable network link underneath HTTP. The browser's WebSocket interface has no automatic reconnection or replay, so your code must reconnect, back off and resync.
| Question | Polling | Server-Sent Events | WebSockets |
|---|---|---|---|
| Direction | Browser asks on a timer | Server to browser only | Both ways |
| Reconnection | Each request is new | Built in, with a retry delay | Your code |
| Catching up after a drop | Ask for data since the last timestamp | Last-Event-ID header | Your own resume message |
| In a hidden Chromium tab | A 100 ms timer fell to once a second, then once a minute | About 193 messages a second still arrived | A message every second still arrived, for 7 minutes |
| Main trap | Wasted requests when nothing changed | 6 connections per browser per site without HTTP/2 | Reconnection and slowing a fast sender are yours |
The connection cap matters on dashboards. MDN warns that without HTTP/2, the newer HTTP version that runs many requests over one connection, SSE is limited to 6 open connections per browser per domain. The limit is shared across tabs.
MDN reports, as of 28 October 2026, that Chrome and Firefox marked this Won't fix. Over HTTP/2 the limit is negotiated, 100 streams by default.
So send every chart's updates over one stream and route them by a field such as a tile or chart id. The HTML standard also suggests sharing one EventSource across tabs through a shared worker, a background script that all tabs of a site can share.
Two server habits help. The HTML standard says some legacy proxies drop idle connections, and suggests a comment line every 15 seconds or so. For WebSockets, bufferedAmount reports bytes queued by send() but not yet sent, and the standard's example sends only when it is empty.
The SevyDevy guide on WebSockets vs SSE vs long polling covers the protocols in depth. Here, what matters is that push keeps flowing while a tab is hidden, as the background tab section shows.
How do you batch live updates with requestAnimationFrame?
Store each message, and let one requestAnimationFrame callback write everything that changed since the last frame. For tiles keep only the newest value; for charts append to the ring buffer and redraw once.
The naive version updates the page inside every message handler. It is shown here because it is wrong at 200 messages a second. For a chart, that means about 200 full redraws a second while the screen shows only 60.
For tiles, scattered writes are cheap until some code reads layout in between, such as measuring a tile. Then the browser must recalculate layout on the spot, many times a frame.
// Keep only the newest value per tile, and write to the page at most once per frame.
const latest = new Map(); // tile id to its newest value
let frameBooked = false;
function onMessage(tileId, value) {
latest.set(tileId, value); // overwrite: older values are dropped
if (!frameBooked) {
frameBooked = true;
requestAnimationFrame(flush); // paused while the tab is hidden
}
}
function flush() {
frameBooked = false;
for (const [tileId, value] of latest) {
document.getElementById(tileId).textContent = value;
}
latest.clear();
window.renders = (window.renders || 0) + 1; // counters to check it works
}
const source = new EventSource('/events');
source.onmessage = function (e) {
const update = JSON.parse(e.data); // for example {"tile":"orders","value":"412"}
window.messages = (window.messages || 0) + 1;
onMessage(update.tile, update.value);
};We ran this exact file in Chromium 153 against a local server sending about 200 SSE messages a second, taking turns across three tiles. In 6 visible seconds, 1,150 messages arrived and the batcher flushed 358 times, about once per frame.
What just happened? A counter on the page saw 1,070 text writes, so most values were still shown, because each tile changed about once per frame. The batcher drops a value only when one tile changes twice before the next frame.
The win is the grouping. All writes happen in one pass per frame, and a chart drawn this way redraws at most once per frame, not 200 times a second. When the stream stopped, all three tiles showed exactly the last value sent.
While the tab was hidden for 10 seconds, 1,925 messages arrived and the page did no writes at all, because requestAnimationFrame does not run in a hidden Chromium tab. Within 100 ms of showing the tab again, it had drawn 7 frames with the newest values.
To check it on your own page, open the browser console and compare window.messages with window.renders. Renders should stay at or below the screen's refresh rate while messages climb faster.
Latest value wins is right for tiles that show a current reading. For counters that must add up, such as orders today, add each message into the stored total instead of dropping it.
How do you downsample a long time series for a chart?
Reduce each series to about one to four points per pixel column before drawing, using a method that keeps the visible shape. For the operations dashboard, 86,400 points become about 800 to 3,200 for an 800-pixel chart.
The naive version keeps every 108th point. It is wrong for monitoring because short spikes fall between the kept points. In our test it kept none of three spikes and missed a deep dip.
LTTB, short for Largest-Triangle-Three-Buckets, comes from Sveinn Steinarsson's 2013 master's thesis at the University of Iceland, Downsampling Time Series for Visual Representation. The thesis abstract reports that, in an online survey, methods based on triangle areas performed consistently well.
LTTB splits the series into equal buckets and always keeps the first and last points. In each bucket it keeps the point that forms the largest triangle with the last kept point and the average of the next bucket.
One LTTB step
Drag to exploreNotice that only one point survives per bucket. A big triangle means the point bends the line, which is why spikes usually survive, but two extremes in one bucket cannot both survive.
Diagram connections:
- Last kept point A to Triangle area per candidate (corner one)
- Current bucket to Triangle area per candidate (each candidate)
- Next bucket average C to Triangle area per candidate (corner three)
- Triangle area per candidate to Keep largest-area point B (largest wins)
- Keep largest-area point B to B becomes the new A (keep B)
- B becomes the new A to Last kept point A (next bucket)
- Last kept point A: Chosen in the previous bucket
- Current bucket: About 108 candidate points here
- Next bucket average C: Mean time and mean value
- Triangle area per candidate: Area of A, the candidate and C
- Keep largest-area point B: Exactly one point per bucket
- B becomes the new A: Move on to the next bucket
Min-max downsampling keeps the lowest and the highest point of every bucket, in time order. With one bucket per pixel column, it draws the full vertical reach of every column, which is what the eye reads as a spike.
A third method, M4 from a 2014 database research paper, keeps the first, last, lowest and highest point of every pixel column. It costs more points, but the paper shows it gives an error-free picture for simple two-colour line charts.
Pause and predict: in our made-up day of 86,400 per-second readings, which methods kept all three spikes? LTTB, min-max and M4 all did. The difference was in the fine texture of the line.
| Method | Points out | Spikes kept (of 3) | Dip kept | Mean pixel error | Columns off by 2+ pixels |
|---|---|---|---|---|---|
| Every 108th point (naive) | 801 | 0 | No | 10.31 | 792 of 800 |
| LTTB to 800 points | 800 | 3 | Yes | 5.56 | 699 of 800 |
| Min-max, 400 buckets | 800 | 3 | Yes | 7.19 | 650 of 800 |
| Min-max, one bucket per pixel | 1,600 | 3 | Yes | 0.24 | 3 of 800 |
| M4, one bucket per pixel | 3,165 | 3 | Yes | 0.00 | 0 of 800 |
What just happened? LTTB kept the shape and the spikes but thinned the noisy band, so 699 of 800 columns drew a little differently. Min-max with one bucket per pixel matched the full chart almost exactly using 1,600 points.
So choose by the question the chart answers. Use min-max or M4 when people hunt for spikes and outages. Use LTTB when the chart shows trends and a clean line reads better than a noisy band.
LTTB vs min-max: where does each downsampling method fail?
LTTB fails when two extremes share one bucket, and min-max can draw false connecting lines between columns. The code below shows the first failure on a small series.
// LTTB: keep one point per bucket, the one that makes the largest triangle.
// target must be at least 3 and smaller than the number of points.
function lttb(xs, ys, target) {
const n = xs.length;
const keep = [0]; // always keep the first point
const size = (n - 2) / (target - 2); // bucket width
let a = 0; // index of the last point kept
for (let i = 0; i !== target - 2; i++) {
const nextFrom = Math.floor((i + 1) * size) + 1;
const nextTo = Math.min(Math.floor((i + 2) * size) + 1, n);
let cx = 0, cy = 0; // average of the next bucket
for (let j = nextFrom; j !== nextTo; j++) { cx += xs[j]; cy += ys[j]; }
cx /= nextTo - nextFrom;
cy /= nextTo - nextFrom;
const from = Math.floor(i * size) + 1, to = Math.floor((i + 1) * size) + 1;
const areas = []; // twice the triangle area, per candidate
for (let j = from; j !== to; j++) {
areas.push(Math.abs((xs[a] - cx) * (ys[j] - ys[a]) - (xs[a] - xs[j]) * (cy - ys[a])));
}
// Spreading (...) fails for buckets above about 120,000 points in Node 22; loop then.
a = from + areas.indexOf(Math.max(...areas)); // first largest wins, as in the thesis code
keep.push(a);
}
keep.push(n - 1); // always keep the last point
return keep;
}
// Min-max: the lowest and the highest point of every bucket, in time order.
function minMax(ys, buckets) {
const n = ys.length, keep = [];
for (let b = 0; b !== buckets; b++) {
const from = Math.floor((b * n) / buckets);
const part = ys.slice(from, Math.floor(((b + 1) * n) / buckets));
const lo = from + part.indexOf(Math.min(...part));
const hi = from + part.indexOf(Math.max(...part));
if (lo === hi) keep.push(lo);
else keep.push(Math.min(lo, hi), Math.max(lo, hi));
}
return keep;
}
// 10,000 calm readings near 40, with a spike at 5000 and a dip at 5040.
const xs = Array.from({ length: 10000 }, function (_, i) { return i; });
const ys = xs.map(function (i) { return 40 + 5 * Math.sin(i / 300); });
ys[5000] = 98;
ys[5040] = 2;
const a = lttb(xs, ys, 100);
const b = minMax(ys, 100);
console.log('LTTB points:', a.length, 'spike kept:', a.includes(5000), 'dip kept:', a.includes(5040));
console.log('min-max points:', b.length, 'spike kept:', b.includes(5000), 'dip kept:', b.includes(5040));Running it with Node.js, the JavaScript runtime outside the browser, prints that LTTB kept 100 points, kept the spike and lost the dip. Min-max kept 200 points and both extremes. The spike and dip sit 40 readings apart, inside one LTTB bucket of about 102 readings.
The code avoids the less-than and greater-than signs by finding the largest area with Math.max. We checked this lttb function against the reference code Steinarsson published under the MIT licence; on five random series, both returned identical points.
Min-max has the opposite weak spot. The M4 paper, published in the Proceedings of the VLDB Endowment in 2014, shows that skipping each column's first and last points can draw wrong lines between columns.
Our raster test agreed. Per-pixel min-max left 3 columns off by 2 or more pixels, while M4 left none.
Cost is rarely the deciding factor, but it is not free. On our 2-CPU test machine, downsampling 1,000,000 points to 800 took 3.6 to 8.1 ms with LTTB and 2.2 to 3.0 ms with min-max, across seven runs in Node 22.
LTTB alone can use a third or more of a 10 ms frame, so run it in a Web Worker, or on the server for long time ranges. Downsample again whenever the chart's width or zoom changes, because the bucket count follows the pixel count.
Keep the raw series in the ring buffer and treat the downsampled copy as a throwaway view. That way zooming in can always return to real points.
What happens to timers and charts in a background tab, measured in Chromium 153?
In Chromium 153, a hidden tab stops requestAnimationFrame completely and slows timers to once a second. After about five minutes hidden, repeating timers drop to once a minute. Pushed messages keep arriving at full speed.
The HTML standard allows this. Its timer steps let the browser optionally wait a further implementation-defined length of time, and its page visibility section defines the hidden and visible states and the visibilitychange event.
The Chrome team's post on heavy throttling of chained timers, which arrived in Chrome 88, describes the levels. A timer is chained when it is set from inside another timer's callback, and every setInterval repeat counts as part of a chain.
Intensive throttling, once a minute, applies when the page has been hidden over 5 minutes and the chain count is 5 or more. The page must also have been silent for 30 seconds, with WebRTC, the browser's live audio and video calling interface, not in use.
We measured it on Playwright's Chromium 153 build, driven directly through its remote-control interface, the DevTools protocol. Playwright's own launcher passes flags that turn background throttling off. Each tab ran a 100 ms setInterval and a chained 100 ms setTimeout.
| Phase | setInterval and chained setTimeout (100 ms) | requestAnimationFrame | Pushed messages |
|---|---|---|---|
| Visible (after returning to the tab) | 10 per second | 60 per second | About 194 SSE messages per second |
| Hidden, first 20 seconds | 1 per second | 0 | About 193 SSE messages per second |
| Hidden, minutes 1 to 5 | 1 per second | 0 | Not streamed in this run |
| Hidden, minutes 6 and 7 | 1 per minute | 0 | Not streamed in this run |
| Hidden with an open WebSocket, minutes 6 and 7 | 1 per minute | Not measured | 1 WebSocket message per second, all delivered |
One finding contradicts a popular reference. MDN's Page Visibility API page says, as of 28 October 2026, that tabs using real-time connections such as WebSockets go unthrottled. In our Chromium 153 run, timers were throttled the same with a WebSocket open and receiving a message every second.
That may describe older Chrome behaviour. The Chrome 88 post's test command line includes a flag named AllowAggressiveThrottlingWithWebSocket, which suggests WebSocket pages were once treated differently.
We did not test Firefox or Safari. MDN reports that Firefox desktop enforces a minimum 1 second timeout in inactive tabs. Treat all of this as browser behaviour that can change between versions, not as a contract.
What a hidden dashboard tab does
Drag to exploreNotice that drawing stops but the network does not. A bounded store absorbs the stream, and the visible event triggers one catch-up draw instead of replaying every missed message.
Diagram connections:
- Tab visible to Tab hidden (user switches tab)
- Tab hidden to Hidden over 5 minutes? (time passes)
- Hidden over 5 minutes? to Intensive throttling (yes)
- Hidden over 5 minutes? to Tab hidden (no: stays at once a second)
- Tab hidden to Messages keep arriving (network still flows)
- Messages keep arriving to Bounded store (overwrite, do not queue)
- Intensive throttling to Tab visible again (user returns)
- Bounded store to Tab visible again (newest values ready)
- Tab visible: rAF 60 per second, timers on time
- Tab hidden: rAF stops, timers once per second
- Hidden over 5 minutes?: Chained timers, silent, no WebRTC
- Intensive throttling: Chained timers run once per minute
- Messages keep arriving: SSE and WebSocket are not paused
- Bounded store: Latest value per tile, ring buffers
- Tab visible again: visibilitychange: resync, draw once
For the operations dashboard this gives four rules. First, drive all drawing from requestAnimationFrame, so a hidden tab costs nothing. Second, do not poll with timers in the background, since a 5 second poll can become a 60 second poll.
Third, listen for visibilitychange. When the tab becomes visible, ask the server for anything missed, using Last-Event-ID or a since-timestamp, and draw once.
Fourth, keep the store bounded, with one value per tile and a fixed ring buffer per chart. Then 193 messages a second for an hour cannot grow memory while nobody is looking.
How do you keep chart layouts correct when the page resizes?
Watch each chart's container with ResizeObserver, then resize the canvas and downsample again for the new width in the next frame. The window's resize event is not enough, because sidebars and grid changes resize charts without resizing the window.
ResizeObserver reports changes to an element's content box (the area inside its padding) or border box (the area out to its border). The draft from the W3C, a web standards body, says an observation fires when observing starts, if the element is rendered and not 0 by 0 in size. So you get a first size for free.
In our Chromium 153 test, one callback arrived after observe(). Five width changes made in one task produced a single callback with the final width, so the browser already merges changes within a frame.
Two traps catch dashboards. First, setting a canvas's width attribute clears its pixels, even when the value is unchanged; in our test a pixel's alpha, its opacity value, dropped from 255 to 0. So redraw in the same frame as the resize.
Second, a callback that changes the size it observes creates a loop. Chromium stopped it and fired an error event saying ResizeObserver loop completed with undelivered notifications. MDN suggests deferring such changes with requestAnimationFrame, or skipping sizes you have already applied.
A CSS pixel is the unit in your styles, and a high-density screen packs two or more device pixels into each one. For sharp lines, size the canvas bitmap in device pixels. The device-pixel-content-box option reports that size directly; at a forced scale factor of 2, a 400 CSS pixel box reported 800 device pixels in our run. Where it is missing, multiply the CSS size by devicePixelRatio.
Observe the container
Call observe on each chart's wrapper element with the device-pixel-content-box option where supported. One observer can watch all 12 charts.
Record the size only
In the callback, store each chart's new width and height and book one requestAnimationFrame. Do not change layout inside the callback.
Resize the bitmap
In the frame, set the canvas width and height to the device pixel size, and its CSS size to the CSS pixel size. Then scale the drawing context by the same ratio, as MDN's canvas guide shows.
Downsample for the new width
Recompute buckets from the raw ring buffer, so the chart again draws about one to four points per pixel column.
Redraw in the same frame
Draw straight away, because resizing cleared the canvas. A blank flash means the redraw slipped into a later frame.
How do you make dashboard charts accessible?
Give every chart a text alternative that states its point, offer the numbers as a table, never rely on colour alone, and let people pause live updates. Each of these maps to a WCAG 2.2 success criterion.
WCAG 2.2, the Web Content Accessibility Guidelines, became a W3C Recommendation on 5 October 2023; the current edition is dated 12 December 2024. Its criterion 1.1.1 asks for text alternatives for non-text content, and 1.4.1 forbids colour as the only way to convey information.
Criterion 1.4.11 asks for a 3:1 contrast ratio for graphical objects needed to understand the content, such as chart lines against their background. Criterion 2.2.2 asks that auto-updating information shown beside other content can be paused, stopped or hidden.
SVG and canvas differ here. We checked Chromium's accessibility tree, the structure screen readers read. An SVG with role img, which tells assistive technology to treat it as one picture, got a readable name from its title element. So did a canvas with role img and an aria-label, an attribute holding its spoken name. A bare canvas showed up as Canvas with no name.
The HTML standard treats a canvas element's contents as fallback content. When we put a small table inside a canvas, Chromium's accessibility tree held the whole table with caption, headers and cells.
ARIA (Accessible Rich Internet Applications) attributes describe page parts to assistive technology. A live region, set with aria-live, is an element whose changes screen readers announce.
MDN advises against announcing ticker-style updates every time unless that is the page's main purpose; for those it suggests aria-live off. So leave the KPI tiles themselves without aria-live.
Instead keep one separate, visually quiet element with aria-live set to polite. Write a short sentence into it only when a tile crosses a threshold, such as payment errors above 2 percent.
- Name each chart with its takeaway, such as orders per second steady near 40 with one spike at 13:36, not just Orders chart.
- Offer a table or a download of the visible numbers next to each chart.
- Mark alert states with a shape or a text label as well as red, and check 3:1 contrast for lines and markers.
- Add one pause button for all live updates, and skip chart animations when prefers-reduced-motion, the device setting for less motion, is set to reduce.
- Announce threshold crossings through one separate polite live region, never by making the tiles themselves live.
The WAI-ARIA Graphics Module, a W3C Recommendation since 2018, adds roles such as graphics-document for graphics whose layout carries meaning. It says detailed support for data-rich charts is future work, so text alternatives and tables remain the reliable route.
How do you explain dashboard frontend design in a system design interview?
Lead with the budgets, then the pipeline, then the failure modes. Interviewers want to see you size the problem before you name a chart library.
Size the data
12 charts, 86,400 points each, 800 pixels wide, 200 messages a second at peak. That is about 108 points per pixel column, so drawing raw data is out.
State the frame budget
16.7 ms per frame at 60 Hz, about 10 ms of it for your code, and half that on a 120 Hz screen.
Choose the transport
One SSE stream for all charts with Last-Event-ID resume, or WebSockets if the client also sends a lot. Mention the 6-connection cap without HTTP/2.
Decouple messages from frames
A bounded latest-value store and ring buffers, flushed by one requestAnimationFrame callback per frame.
Downsample to pixels
Min-max or M4 per pixel column for monitoring, LTTB for trends, in a worker or on the server for long ranges.
Pick the renderer
SVG for small charts, canvas as separate marks head towards 10,000, and OffscreenCanvas in a worker for the heaviest ones.
Handle hidden tabs and resizes
No drawing while hidden, then resync on visibilitychange. ResizeObserver feeds each new width into downsampling.
Close with accessibility
A text alternative with the takeaway, a data table, no colour-only meaning and a pause control for live updates.
Questions people ask
- What is dashboard frontend design in simple words?
- It is the plan for the browser side of a live dashboard. It decides how numbers arrive, where they wait, when they are drawn and how many points each chart draws. The aim is to fit every update into one frame, about 16.7 ms on a 60 Hz screen, so charts stay smooth and clicks stay responsive.
- Is SVG or canvas better for real-time charts?
- SVG is better for small charts and canvas for big ones. In our Chromium 153 test, every method held 60 frames a second at 1,000 marks. At 10,000 marks, SVG circles missed 113 of 119 frames while canvas squares missed none. SVG gives easy styling, events and accessibility; canvas avoids one DOM element per mark.
- Should a dashboard use WebSockets or Server-Sent Events?
- Use Server-Sent Events when the dashboard mostly receives data. It has built-in reconnection and the Last-Event-ID header for catching up. Use WebSockets when the browser also sends a lot back, and write your own reconnection. Either way, send every chart over one connection, because without HTTP/2 browsers allow only 6 SSE connections per site.
- What is LTTB downsampling?
- LTTB, or Largest-Triangle-Three-Buckets, comes from Sveinn Steinarsson's 2013 master's thesis. It splits a series into buckets and keeps one point per bucket, the one forming the largest triangle with the last kept point and the next bucket's average. It keeps a line's shape well, but two extremes in one bucket cannot both survive.
- Why do my dashboard timers slow down in a background tab?
- Browsers throttle hidden tabs to save power. In our Chromium 153 run, a 100 ms timer fired once a second while hidden, and once a minute after about five minutes. An open WebSocket did not prevent this. requestAnimationFrame stopped completely, so draw from it and resync when visibilitychange reports the tab visible again.
- How many points should a line chart draw?
- About one to four points per pixel column of the chart, four when you use M4. An 800-pixel chart needs roughly 800 to 3,200 points, whatever the raw series length. In our test, min-max with one bucket per pixel used 1,600 points and matched the full 86,400-point chart to within a quarter pixel on average.
- How do I make a canvas chart accessible?
- Give the canvas a role of img and an aria-label, an attribute holding its spoken name, that states the takeaway. Or put a data table inside the canvas element as fallback content. In our Chromium test, a bare canvas had no accessible name, while fallback table content reached the accessibility tree. Also avoid colour-only meaning and offer a pause control for live updates.
Sources and further reading
- WHATWG HTML Standard: Timers (4 ms nesting clamp, optional implementation-defined wait)
- WHATWG HTML Standard: Page visibility (hidden and visible states, visibilitychange)
- WHATWG HTML Standard: Server-sent events (reconnection, retry, Last-Event-ID, keep-alive comments)
- WHATWG HTML Standard: The canvas element (fallback content)
- WHATWG WebSockets Standard (bufferedAmount and its example)
- RFC 6455: The WebSocket Protocol (December 2011)
- W3C Resize Observer (First Public Working Draft, 11 February 2020)
- W3C WCAG 2.2 (Recommendation 5 October 2023; current edition 12 December 2024): 1.1.1, 1.4.1, 1.4.11, 2.2.2
- W3C WAI-ARIA Graphics Module 1.0 (Recommendation, 2 October 2018)
- MDN: Window.requestAnimationFrame() (refresh rate, paused in background tabs)
- MDN: Page Visibility API (background throttling policies, WebSocket claim)
- MDN: setTimeout(), reasons for delays longer than specified
- MDN: EventSource (6-connection limit without HTTP/2)
- MDN: ResizeObserver (loop error and fixes)
- MDN: Optimizing canvas
- MDN: OffscreenCanvas (workers, browser availability)
- MDN: aria-live (polite, assertive, off; ticker advice)
- MDN: prefers-reduced-motion
- Chrome for Developers: Heavy throttling of chained JS timers beginning in Chrome 88
- web.dev: Rendering performance (16.66 ms frame, 10 ms for your work, pixel pipeline)
- Steinarsson (2013), Downsampling Time Series for Visual Representation, MSc thesis, University of Iceland (Skemman record)
- Catalogue record and abstract of the Steinarsson thesis (AGRIS)
- flot-downsample: Steinarsson's reference LTTB code (MIT licence)
- Jugel et al., M4: A Visualization-Oriented Time Series Data Aggregation, PVLDB 7(10), 2014