Skip to content

P 11 Canvas Grid

Karthik P edited this page Oct 27, 2026 · 1 revision

P-11: Canvas-rendered grid vs DOM virtualization

Context: After P-04, the results grid keeps only the rows in view in the page, about 30 rows. Narrow results scroll at the display's frame rate at any size, but wide results (50 columns) still drop frames while the scrollbar is dragged: every dragged frame lays out and paints a screenful of new rows, about 1,500 DOM cells. Drawing those cells on a canvas instead skips the per-cell style, layout and paint work.

Hypothesis: Drawing the grid's rows on a canvas gives a higher scroll frame rate on wide tables than the DOM-virtualized grid.

Change: #53, experiment branch perf/53-canvas-grid (not merged).

  • Flag: ?grid=canvas switches the grid to the canvas renderer. Without it, the grid is the DOM grid from P-04.
  • Canvas under a DOM layer: src/features/results/CanvasBody.tsx draws the rows in view on a canvas the size of the grid's viewport, redrawn on each scroll. src/features/results/canvas-grid.ts draws the cells, borders, selection and active cell in the same places as the DOM grid, with colours and font read from the grid's computed style. The DOM cells stay in the page over the canvas, transparent, so screen readers, keyboard navigation, the aria-* positions, clicks and tooltips work as before.
  • Forced colours: in forced colours (such as Windows High Contrast) the grid falls back to the DOM, because the browser applies system colours to DOM cells but not to a canvas.
  • Size: about 600 lines of code across the renderer, the layout code it shares with the DOM grid (grid-layout.ts), the grid and their tests.

Method:

  • Build: 5dc2aea on the experiment branch, clean working tree, served by vite preview with the production headers. Both renderers are the same build; only the URL differs.
  • Scenarios: scroll-10k-wide and scroll-100k-wide from P-04, and scroll-10k-wide-canvas and scroll-100k-wide-canvas, the same scenarios with ?grid=canvas. Each shows SELECT * … LIMIT <rows> of the wide dataset, then scrolls for 5 s at a steady 3,000 px a second (wheel) and 5 s from the first row to the last (drag), sampling every frame. All four ran back to back in one session.
  • Iterations: 3 warm-up and 10 measured iterations per scenario; median and p95 reported.
  • Environment: the reference laptop from P-01 (Intel Core i5-8250U, 24 GB, Windows 11 Pro 10.0.26300), on mains power. Chromium 156.0.8078.4 headless, which draws at 75 Hz: a smooth frame is 13.3 ms.

All times are in milliseconds, given as median / p95. Slow frames are frames over 25 ms, counted per 5-second scroll.

10,000 wide rows

Metric DOM Canvas Change (median)
renderMs 15.7 / 19.7 14.1 / 16.7 −10%
firstRowsRenderMs 75.2 / 111.9 82.0 / 113.4 +9%
memoryBytes (MB) 56.4 / 56.8 57.7 / 58.2 +1.3 MB
wheelFrameP95Ms 13.6 / 13.7 13.6 / 13.7 none
wheelSlowFrames 2.0 / 4.0 0.0 / 2.0 −2
dragFrameMedianMs 63.3 / 66.7 56.7 / 66.7 −10%
dragFrameP95Ms 80.0 / 80.1 80.0 / 93.3 none
dragSlowFrames 81.0 / 83.5 80.5 / 83.5 none
dragLongestFrameMs 91.6 / 103.3 96.8 / 118.0 +6%

100,000 wide rows

Metric DOM Canvas Change (median)
renderMs 19.3 / 23.2 15.5 / 20.3 −20%
firstRowsRenderMs 98.3 / 114.7 101.6 / 114.5 +3%
memoryBytes (MB) 91.5 / 91.9 92.8 / 93.1 +1.2 MB
wheelFrameP95Ms 13.6 / 13.7 13.6 / 13.7 none
wheelSlowFrames 1.0 / 2.5 0.0 / 0.0 −1
dragFrameMedianMs 66.6 / 66.7 66.7 / 66.7 none
dragFrameP95Ms 80.0 / 80.1 80.0 / 87.4 none
dragSlowFrames 80.0 / 84.5 79.5 / 82.1 none
dragLongestFrameMs 84.5 / 103.3 87.6 / 95.9 +4%

Findings

  1. Wheel scrolling was already smooth; the canvas removes the last slow frames. Both renderers wheel-scroll at 75 fps, with a 95th-percentile frame of 13.6 ms. The DOM grid has 1–2 slow frames in 5 seconds; the canvas has none.
  2. Rendering the rows is 10–20% faster on a canvas. renderMs falls from 15.7 to 14.1 ms at 10,000 rows and from 19.3 to 15.5 ms at 100,000. Time to first rows doesn't improve: it is 3–9% slower, within run-to-run noise.
  3. Drag scrolling doesn't change. Dragged frames take about 66.7 ms at the median in both renderers, 15 frames a second, with about 80 slow frames per 5-second drag. The lower 10,000-row canvas median (56.7 ms) comes from runs that varied between about 53 and 67 ms in both renderers, and the 95th-percentile and longest frames are the same or worse. Whatever bounds a dragged frame on wide results, painting cells isn't it, so the hypothesis doesn't hold.
  4. Memory rises by about 1.3 MB at both sizes, for the canvas.

Trade-offs:

  • Accessibility gaps against the DOM grid. The transparent DOM layer keeps screen readers and keyboard navigation working, but:
    • the browser's Find (Ctrl+F) matches the transparent cells, so it highlights nothing visible;
    • selecting text with the mouse selects the invisible DOM text, not what's drawn;
    • wide characters (CJK, emoji) are truncated differently on the canvas than in the DOM cells over it;
    • forced colours are checked only when the page loads, so turning High Contrast on afterwards leaves the canvas in the wrong colours;
    • when the scroll position is set without a scroll event, the canvas lags the DOM layer by one frame.
  • More code to own. About 600 lines, and two renderers that must draw every cell, border and selection state in the same place.
  • Memory: about 1.3 MB more with a result on screen.

Limitations:

  • Wide results only. Narrow results already scroll at 75 fps with the DOM grid (P-04), so they weren't run.
  • Chromium only, reference laptop only, no throttled profile, as in P-04.
  • Headless frame times and synthetic scrolling, as in P-04: frames are drawn at 75 Hz without a real display, and the scenarios set scrollTop once a frame instead of going through the browser's own wheel or scrollbar handling.
  • The dragged frames weren't profiled. Why they stay at about 66.7 ms in both renderers wasn't traced.

Evidence:

  • Raw results: bench/results/2026-10-26T13-13-46Z-5dc2aea.json, all four scenarios, on the experiment branch.
  • To reproduce: check out perf/53-canvas-grid, run pnpm bench:data, then pnpm bench --scenario scroll-10k-wide --scenario scroll-10k-wide-canvas --scenario scroll-100k-wide --scenario scroll-100k-wide-canvas. To try the canvas by hand, open /app/?grid=canvas on that branch.

Conclusion: Don't adopt; keep the DOM grid. The canvas removes 1–2 slow wheel frames and renders rows 10–20% faster, but drag scrolling, the case it was meant to fix, is unchanged. In exchange it adds about 1.3 MB of memory, the accessibility gaps above and about 600 lines of code. The branch is kept for reference and not merged. Revisit only if profiling drag scrolling shows that painting cells is the bottleneck.

Clone this wiki locally