We rewrote our custom visualisation renderers from SVG to be in Canvas

yomieluwande1 pts0 comments

Our new and faster profiler dashboard | Polar Signals

Sign InSign UpSchedule a call

Sign InSign Up<br>Schedule a call

Our new and faster profiler dashboard<br>Learn more about our new and improved profiler dashboard and how we made the switch from SVG to Canvas

Authors:<br>Yomi E, Manoj V.

JUL 14, 2026<br>14 min

We've been working hard at a revamp of our profiling dashboard and we wanted to share some of our learnings and improvements we've observed along the way, backed by numbers.

This is not the first time we have written about making flame graphs fast. Last year we parallelised the SVG render and took the initial render of a large query from around eight seconds to about 91 milliseconds.

But SVG has a ceiling, and that ceiling is made up of DOM nodes. Every frame in a flame graph is a and a . Therefore a large profile is tens of thousands of frames, which is tens of thousands of DOM nodes and for each node the browser has to style, lay out, paint and keep in a tree it can later garbage collect. To stop that from freezing the tab, the old renderer drew nodes in batches of roughly 500 per frame. This worked, but batching the DOM is only just treating a symptom. The fundamental problem here was that we were asking the browser to manage an enormous amount of structure.

To be honest, we more or less said it at the end of that post: we suspected the next real gain was to stop using the DOM for the flame graph, even though we were unsure as to how much Canvas would actually buy us. This post is us answering our own open question.

tl:dr Why is the new profiler dashboard faster?

The old renderer was drawn with SVG, one DOM element and a small JavaScript object for every one of its tens of thousands of frames, all of which the browser then had to style, lay out, paint, and garbage-collect.

The new one draws the same data on a Canvas: it reads the numbers straight from their columns and paints pixels, with no per-frame element or object at all. This makes zooming, resetting, and scrolling feel instant, because none of them touch the DOM any more.

A flame graph is made up of frames

Let's go through the differences between the SVG and Canvas HTML elements with respect to rendering flame graphs.

The SVG and the DOM are retained-mode APIs in the browser while the Canvas is an immediate-mode API. What does this mean?

In retained-mode, the browser is handed a tree of elements and it owns them. It is responsible for handling layout changes and painting for every element, and because they live in a tree, any change forces a re-computation of styles and layout even for other elements in the tree i.e. the browser keeps a model of what you drew and re-renders it for you.

This works well for static documents but can be a lot of bookkeeping for a complex component (like a flame graph) that is essentially thousands of coloured frames with text on them.

(If you write React, this is the world you already live in: you re-describe the UI, and React keeps a DOM tree in sync with it for you. That is retained mode with a friendly face on top.)

In immediate-mode API, there is no tree. Instead, there is a drawing context, and you can issue draw commands like fillRect or fillText, and the browser turns them into pixels and forgets about it. With no nodes to style, or boxes to lay out, there is nothing left behind for the garbage collector and the browser's background chore of reclaiming memory nobody is using any more is eliminated.

For us especially, the cost of rendering the flame graph in Canvas changes from scaling with the number of frames the browser has to remember to scaling with how many you actually draw.

html,body{margin:0;padding:0;background:transparent;-webkit-font-smoothing:antialiased}svg.anim{display:block;box-sizing:border-box}svg.anim *{animation-play-state:paused}svg.anim.in-view *{animation-play-state:running}@media (prefers-reduced-motion:reduce){svg.anim.in-view *{animation-duration:.001s!important;animation-delay:0s!important;animation-iteration-count:1!important}}<br>#animNodes .t{font:600 16px Inter,ui-sans-serif,system-ui;fill:#040406}<br>#animNodes .st{font:500 12px Inter,ui-sans-serif,system-ui;fill:#71717A}<br>#animNodes .hA{font:700 12px Inter,ui-sans-serif,system-ui;fill:#b45309;letter-spacing:.02em}<br>#animNodes .hB{font:700 12px Inter,ui-sans-serif,system-ui;fill:#1d4ed8;letter-spacing:.02em}<br>#animNodes .mono{font:600 13px ui-monospace,SFMono-Regular,Menlo,monospace}<br>#animNodes .cap{font:500 11px Inter,ui-sans-serif,system-ui;fill:#9194A1}<br>#animNodes .div{stroke:#E2E3E9;stroke-width:1}<br>#animNodes .dpBar{fill:#f59e0b;stroke:#d97706;stroke-width:.6;opacity:0;transform-box:fill-box;transform-origin:top}<br>#animNodes.in-view .dpBar{animation:dpPop .4s cubic-bezier(.2,.85,.3,1) both;animation-delay:var(--d)}<br>@keyframes dpPop{0%{opacity:0;transform:translateY(-6px) scaleY(.12)}100%{opacity:1;transform:none}}<br>#animNodes .dpG{clip-path:inset(0 100% 0 0)}<br>#animNodes.in-view .dpG{animation:dpWipe 1.05s...

animnodes browser animation canvas flame font

Related Articles