Canvas Performance Optimization Techniques for Stock K-Line Charts
This article explains how to improve the rendering speed of highly customized stock K-line (candlestick) charts by optimizing canvas drawing commands, using caching strategies, and applying layered rendering to reduce unnecessary state changes and redraws.
Although many excellent visualization libraries such as ECharts and AntV exist in the front‑end community, highly customized business needs like stock market charts still require developers to implement their own solutions. This article introduces the canvas performance‑optimization techniques used in the Snowball K-line project.
Shanghai Composite Index daily K-line chart – Snowball Securities
Optimizing Drawing Commands
A K-line is drawn using the open, high, low, and close prices of each period. The open‑close range forms a rectangular body, which is hollow for an up‑day (bullish) and solid for a down‑day (bearish). In Chinese markets red indicates an up‑day and green a down‑day, while the opposite convention is used in Western markets. Thin lines connect the high and low prices to the body, forming the upper and lower shadows.
Candlestick chart
Observing the structure, the drawing can be split into two vertical lines and a rectangle, i.e., three separate steps.
Draw the line from the highest price to the larger of open or close (upper shadow).
Draw the hollow or solid rectangle formed by open and close (the body).
Draw the line from the lowest price to the smaller of open or close (lower shadow).
Canvas is an imperative drawing system where rendering is achieved by changing the context state and issuing commands such as stroke. Each command incurs a performance cost, so we should minimize unnecessary state changes.
<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #c678dd; line-height: 26px">var</span> canvas = <span style="color: #e6c07b; line-height: 26px">document</span>.getElementById(<span style="color: #98c379; line-height: 26px">"canvas"</span>);
<span style="color: #c678dd; line-height: 26px">var</span> ctx = canvas.getContext(<span style="color: #98c379; line-height: 26px">"2d"</span>);
ctx.lineWidth = <span style="color: #d19a66; line-height: 26px">2</span>;
ctx.strokeStyle = <span style="color: #98c379; line-height: 26px">"#000"</span>;
ctx.beginPath();
ctx.moveTo(<span style="color: #d19a66; line-height: 26px">10</span>, <span style="color: #d19a66; line-height: 26px">10</span>);
ctx.lineTo(<span style="color: #d19a66; line-height: 26px">50</span>, <span style="color: #d19a66; line-height: 26px">10</span>);
ctx.stroke();
ctx.closePath();
</code>By merging the two vertical lines into a single line and treating the hollow body as a filled rectangle, we can reduce the number of drawing commands per candlestick:
Draw one black line connecting the highest and lowest prices.
Draw a stroked solid rectangle representing the open‑close range.
This reduces one line‑drawing operation per candlestick and aligns better with the semantics of a K-line.
Since each candlestick may be bullish or bearish, we can group data by color and draw all bullish candles in one pass and all bearish candles in another, changing the drawing style only twice.
Separate data into two groups based on color (bullish or bearish).
Set bullish style and draw all bullish candles.
Set bearish style and draw all bearish candles.
By changing the brush only twice, we dramatically cut the rendering overhead compared with the original per‑candle state changes. The exact drawing order may need to be adjusted to meet product interaction requirements.
Reasonable Use of Cache
Even after reducing drawing commands, re‑drawing still requires issuing them each frame. Caching the rendered image and reusing it with drawImage can further improve performance.
<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #5c6370; font-style: italic; line-height: 26px">// Get the real canvas and its context</span>
<span style="color: #c678dd; line-height: 26px">const</span> realCanvas = <span style="color: #e6c07b; line-height: 26px">document</span>.getElementById(<span style="color: #98c379; line-height: 26px">"canvas"</span>);
<span style="color: #c678dd; line-height: 26px">const</span> realCtx = canvas.getContext(<span style="color: #98c379; line-height: 26px">"2d"</span>);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Create an off‑screen buffer canvas (or use OffscreenCanvas)</span>
<span style="color: #c678dd; line-height: 26px">const</span> cacheCanvas = <span style="color: #e6c07b; line-height: 26px">document</span>.createElement(<span style="color: #98c379; line-height: 26px">"canvas"</span>);
<span style="color: #c678dd; line-height: 26px">const</span> cacheCtx = cacheCanvas.getContext(<span style="color: #98c379; line-height: 26px">"2d"</span>);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Draw shapes onto the off‑screen canvas</span>
drawKlineShapes(cacheCtx, klineShapes[i]);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Copy the cached image to the visible canvas</span>
realCtx.drawImage(cacheCanvas, 0, 0);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// On update, only redraw the cached image</span>
requestAnimationFrame(() => {
realCtx.clearRect(0, 0, realCanvas.width, realCanvas.height);
realCtx.drawImage(cacheCanvas, 0, 0);
});
</code>Off‑screen rendering should be used cautiously when the product requires frequent creation and destruction of graphics, as it still consumes browser resources. Besides reducing draw calls, caching enables features such as drag‑loading and zooming by leveraging drawImage cropping.
<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #5c6370; font-style: italic; line-height: 26px">// Set cache canvas size; make it larger than the real canvas if drag/zoom is needed</span>
cacheCanvas.width = 10000;
cacheCanvas.height = 10000;
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Initial viewport – center region example</span>
var x = cacheCanvas.width / 2 - realCanvas.width / 2;
var y = cacheCanvas.height / 2 - realCanvas.height / 2;
var w = realCanvas.width;
var h = realCanvas.height;
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Initial draw – crop the central part of the cached canvas</span>
context.drawImage(cacheCanvas, x, y, w, h);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Mouse events for dragging</span>
// onmousedown records start point (mx, my) and sets flag=true
// onmousemove when flag is true calculates new point (newX, newY) and updates drawImage parameters
// onmouseup sets flag=false
// Compute new coordinates and draw the moved region
realCtx.drawImage(cacheCanvas, x + (newX - mX), y + (newY - mY), w, h, 0, 0, w, h);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Mouse wheel for zooming</span>
realCtx.drawImage(bufferCanvas, x, y, w / s, h / s, 0, 0, w, h);
</code>Cache can also be used for hit‑testing: render hidden graphics with unique colors, then retrieve the color under the cursor via getImageData to map back to the corresponding shape.
<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #5c6370; font-style: italic; line-height: 26px">// Draw visible shapes</span>
drawShapes(shapesArray);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Draw hidden shapes for picking</span>
drawCacheShapes(shapesArray);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Get image data from the hidden canvas</span>
var cacheImageData = cacheContext.getImageData(0, 0, width, height);
<span style="color: #5c6370; font-style: italic; line-height: 26px">// Click handler – map color to shape index</span>
canvas.onClick = function(ev) {
var point = getPoint(ev.clientX, ev.ClientY);
var color = getCacheColor(point);
var index = colorToNumber(color);
var shape = shapesArray[index];
};
</code>This picking method is simple and precise, but it requires drawing the scene twice and its performance depends on canvas size, so it is rarely used in complex scenes.
Layered Rendering
In many scenarios, static elements are drawn once while interactive elements need frequent updates. By separating static and dynamic parts into different canvas layers, we avoid redrawing unchanged graphics.
Implementing layered rendering is straightforward: create multiple canvas elements, render background elements on a "background‑layer", main chart graphics on a "main‑layer", and UI interactions on a "ui‑layer".
<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><div id="stage">
<canvas id="ui-layer" width="480" height="320"></canvas>
<canvas id="main-layer" width="480" height="320"></canvas>
<canvas id="background-layer" width="480" height="320"></canvas>
</div>
<style>
canvas { position: absolute; }
#ui-layer { z-index: 3; }
#main-layer { z-index: 2; }
#background-layer { z-index: 1; }
</style>
</code>For example, when showing a tooltip and highlighting a candlestick on mouse hover, only the UI layer needs to be updated, leaving the background and main layers untouched.
Layered rendering solves the problem of unnecessary static redraws, but for scenes mixing static and dynamic content heavily, dynamic region partitioning may be required for partial redraws.
Summary
Canvas provides powerful drawing capabilities; for modest numbers of elements the performance bottleneck is rarely felt, but careful ordering of draw calls is essential for more complex charts like K‑lines. Optimizing command sequences, using caching, and applying layered rendering can dramatically reduce rendering latency, which is critical for high‑frequency stock‑trading interfaces. For further optimization guidelines, refer to the MDN canvas performance article.
One More Thing
Snowball's business is rapidly expanding, and the engineering team is looking for talented individuals. If you are interested in building China’s leading online wealth‑management platform, check the original article for hot positions.
Open positions: Senior Front‑end Architect, Android/iOS/FE Engineer, Recommendation Algorithm Engineer, Java Developer.
Snowball Engineer Team
Proactivity, efficiency, professionalism, and empathy are the core values of the Snowball Engineer Team; curiosity, passion, and sharing of technology drive their continuous progress.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
