HTML5 Canvas All Konva performance tips list
Don't want to spend your time with performance issues? Request a performance review.
Why this is important
The HTML5 canvas is efficient at what it does and internally Konva has many features that aim to provide great performance. However, when your project starts to grow in complexity, or when you just have a lot of shapes on the stage, there must inevitably be some negative performance impact.
Optimization targets
The optimizations here focus on two general rules:
-
Compute as little as possible: all computation takes time to complete. Each individual computation may run in a tiny fraction of a second, but the thousands or millions of computations caused by your code, Konva, JavaScript, and the layers below that, will add up to something more observable by the human eye if that super-slick animation or effect is, in fact, jerky.
-
Draw as little as possible: this is important because all drawing has a performance cost. There are two categories of cost - firstly the computation of the drawing which we covered in the point above, and then the movement of the drawing from memory to the screen. Depending on the case, there may also be intermediate off-screen compositing or per-pixel processing. The rule is therefore do as little drawing as possible.
The Stage
-
Optimise stage size - following the rule of 'draw as little as possible', try to avoid creating a large stage because moving all those bytes from memory to screen is going to have a negative impact. There are some tips here that offer alternative approaches to the mega-stage problem!
-
Set a viewport on mobile - Scaling images is a significant performance hit, so for mobile applications set viewport:
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">which will avoid unnecessary scaling of your Konva output. -
Use
Konva.pixelRatio = 1on retina devices - Konva automatically handles pixel ratio adjustments in order to render crisp drawings on all devices. But, just in case you have bad performance on retina devices, setKonva.pixelRatio = 1to reduce the scaling work Konva has to do. This setting might affect the output in some cases, so make sure that quality of the result is ok for you.
Layers
-
Layer Management - under the hood, each Konva layer is a separate HTML5 canvas element which gives some useful capabilities, including the ability to refresh only a layer that changed and so avoid the performance cost of refreshing the entire stage. But with great power great responsibility comes, and each layer has an incremental performance overhead so we should keep the number of layers to a minimum.
-
Use
layer.listening(false)- Konva gives us mouse and touch event listeners on all the shapes we draw. But there is a performance cost for each one, and for a layer with many shapes Konva has to expend many cycles checking which listeners might be triggered. If you have a layer on which none of the shapes need to react to events, take this burden away by settinglayer.listening(false). See Demo. There is a similar point in the shapes section. -
Optimise dragging costs - while you drag a shape across a layer that layer must be redrawn per cycle of the move event listener. To avoid this performance cost, move the shape to a dedicated layer while dragging, then move it back to original layer at drag end. See Demo
Shapes
-
Shape Caching - internally Konva makes an image of your shape and uses that when the shape has to be drawn. Drawing images avoids the overhead of composing the shape from its drawing instructions, and can increase performance impressively for complex shapes and groups.
-
Keep the shapes tidy - each shape in your stage has a cost just to exist. To optimise performance, hide or remove from the layer any objects that become invisible / opacity = 0, or objects that go out of view.
-
Use
shape.listening(false)- as with layers (see point 7 above), Konva looks out for when events should be triggered for shapes, which has a performance cost. Telling a shape to stop listening for events reduces this cost, as explained at Listening false. -
Switch off perfect drawing - In some cases the result of drawing with the HTML5 canvas is not what you might have expected - see the demo for an example Disable Perfect Drawing. Konva does extra work via its perfect drawing feature to put that right, but this comes with a performance cost. By setting
shape.perfectDrawEnabled(false)this cost can be avoided, with no reduction in output quality, when a shape has fill, stroke and opacity. -
Optimize Stroke Drawing - To achieve drawing results that look as expected, Konva makes an extra internal drawing when a shape has both stroke and shadow. Avoid this performance burden by switching off the shadow that Konva adds for the stroke.
Animations
- Optimize Animation - Avoiding unnecessary redraw costs for animation steps that fall between visual changes.
Memory
-
Avoid Memory Leaks - Konva looks after a lot of cases where you might make memory leaks, but bringing shapes and tweens into the world and managing their exit is an area where you can help.
-
Know what a layer costs. Every layer allocates two canvases: a scene canvas at the device pixel ratio, and a hit canvas always at ratio 1. On a 1920 × 1080 stage on a retina screen that is roughly 33 MB plus 8 MB, so about 41 MB per layer before you draw anything. This is why Konva warns above five layers — at that point you are near 200 MB of canvas memory on nothing but empty layers.
-
Mobile Safari enforces a hard ceiling. Past it you get
Total canvas memory use exceeds the maximum limit, reported as 256 MB on some devices and 384 MB on others, and the canvas goes blank rather than degrading. The levers, in order of effect: fewer layers, a smaller stage, andKonva.pixelRatio = 1— which quarters the scene canvas on a 2× screen. Caches count towards the same budget, so release them withnode.clearCache()when a node is no longer visible.
Very large scenes
- Cull what is off screen. Konva draws every node on a layer whether or not it lands inside the stage. For a scene much larger than the viewport, hide the nodes outside it —
visible(false)skips both drawing and hit testing, and is far cheaper than removing and re-adding nodes.
function cull() {
const view = {
x: -stage.x() / stage.scaleX(),
y: -stage.y() / stage.scaleY(),
width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(),
};
layer.children.forEach((node) => {
node.visible(Konva.Util.haveIntersection(view, node.getClientRect()));
});
}
Call it when the view changes — after a pan or a zoom — not on every frame. getClientRect() is not free, so for tens of thousands of nodes keep your own index of positions and test against that instead of asking each node.
- Skip hit detection during a drag. While a node is being dragged Konva does not run hit detection, which keeps dragging cheap. If you need to know what is under the pointer mid-drag — highlighting a drop target, for example — turn it back on and accept the cost:
Konva.hitOnDragEnabled = true; // default is false
- Reach for one shape instead of many. A thousand nodes each with their own attributes, transform and hit region cost far more than one custom shape that draws a thousand things in a single
sceneFunc. You lose per-item events and dragging, so this is a trade, not a free win — see Custom Shape.
Below is a demo showing some of these performance tips in action:
- Vanilla
- React
- Vue
import Konva from 'konva';
// Create stage with good performance settings
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Create layers with performance optimizations
const backgroundLayer = new Konva.Layer({ listening: false });
const mainLayer = new Konva.Layer();
const dragLayer = new Konva.Layer();
stage.add(backgroundLayer);
stage.add(mainLayer);
stage.add(dragLayer);
// Create a shape with caching
const star = new Konva.Star({
x: 200,
y: 200,
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true,
perfectDrawEnabled: false, // performance optimization
});
// Cache the shape for better performance
star.cache();
// Optimize dragging performance
star.on('dragstart', () => {
star.moveTo(dragLayer);
});
star.on('dragend', () => {
star.moveTo(mainLayer);
});
// Create background with listening disabled
const rect = new Konva.Rect({
x: 0,
y: 0,
width: stage.width(),
height: stage.height(),
fill: 'lightgray',
listening: false,
});
backgroundLayer.add(rect);
mainLayer.add(star);
import { Stage, Layer, Star, Rect } from 'react-konva';
import { Portal } from 'react-konva-utils';
import { useState, useRef, useEffect } from 'react';
const App = () => {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 200, y: 200 });
const starRef = useRef(null);
useEffect(() => {
// Cache the shape for better performance
if (starRef.current) {
starRef.current.cache();
}
}, []);
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = (event) => {
setPosition(event.target.position());
setIsDragging(false);
};
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer listening={false}>
<Rect
x={0}
y={0}
width={window.innerWidth}
height={window.innerHeight}
fill="lightgray"
/>
</Layer>
<Layer>
<Portal selector=".drag-layer" enabled={isDragging}>
<Star
ref={starRef}
x={position.x}
y={position.y}
numPoints={6}
innerRadius={40}
outerRadius={70}
fill="yellow"
stroke="black"
strokeWidth={4}
draggable
perfectDrawEnabled={false}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
/>
</Portal>
</Layer>
<Layer name="drag-layer" />
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageSize">
<v-layer :config="{ listening: false }">
<v-rect :config="rectConfig" />
</v-layer>
<v-layer ref="mainLayerRef">
<v-star
ref="starRef"
:config="starConfig"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
/>
</v-layer>
<v-layer ref="dragLayerRef" />
</v-stage>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const stageSize = {
width: window.innerWidth,
height: window.innerHeight
};
const rectConfig = {
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight,
fill: 'lightgray'
};
const starConfig = {
x: 200,
y: 200,
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true,
perfectDrawEnabled: false
};
const starRef = ref(null);
const mainLayerRef = ref(null);
const dragLayerRef = ref(null);
onMounted(() => {
// Cache the shape for better performance
if (starRef.value) {
starRef.value.getNode().cache();
}
});
const handleDragStart = () => {
starRef.value.getNode().moveTo(dragLayerRef.value.getNode());
};
const handleDragEnd = () => {
starRef.value.getNode().moveTo(mainLayerRef.value.getNode());
};
</script>