HTML5 Canvas Optimize Strokes Performance Tip
When drawing shapes with strokes and shadows in Konva, there's an extra internal drawing step that occurs. This is because Konva needs to ensure that the stroke's shadow is drawn correctly. However, this can impact performance, especially when dealing with many shapes.
To optimize performance, you can disable the stroke shadow by setting shadowForStrokeEnabled(false).
This is particularly useful when you don't need the stroke to cast a shadow.
Below is a demo showing the performance difference with and without stroke shadows:
- Vanilla
- React
- Vue
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create shape with shadow for stroke (default)
const circleWithShadow = new Konva.Circle({
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5,
});
// Create shape without shadow for stroke (optimized)
const circleOptimized = new Konva.Circle({
x: 250,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5,
shadowForStrokeEnabled: false,
});
// Add labels
const defaultLabel = new Konva.Text({
x: 50,
y: 170,
text: 'With Stroke Shadow',
fontSize: 16,
});
const optimizedLabel = new Konva.Text({
x: 200,
y: 170,
text: 'Without Stroke Shadow\n(Better Performance)',
fontSize: 16,
});
// Add FPS counter
const fpsText = new Konva.Text({
x: 10,
y: 10,
text: 'FPS: 0',
fontSize: 16,
});
layer.add(circleWithShadow);
layer.add(circleOptimized);
layer.add(defaultLabel);
layer.add(optimizedLabel);
layer.add(fpsText);
// Create animation to demonstrate performance
const anim = new Konva.Animation((frame) => {
circleWithShadow.rotation(frame.time * 0.1);
circleOptimized.rotation(frame.time * 0.1);
// Update FPS counter
fpsText.text('FPS: ' + frame.frameRate.toFixed(1));
}, layer);
anim.start();
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleWithShadowRef = useRef(null);
const circleOptimizedRef = useRef(null);
const fpsTextRef = useRef(null);
useEffect(() => {
const anim = new Konva.Animation((frame) => {
// Rotate circles
circleWithShadowRef.current.rotation(frame.time * 0.1);
circleOptimizedRef.current.rotation(frame.time * 0.1);
// Update FPS counter
fpsTextRef.current.text('FPS: ' + frame.frameRate.toFixed(1));
}, circleWithShadowRef.current.getLayer());
anim.start();
return () => anim.stop();
}, []);
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
{/* Circle with shadow for stroke (default) */}
<Circle
ref={circleWithShadowRef}
x={100}
y={100}
radius={50}
fill="red"
stroke="black"
strokeWidth={4}
shadowColor="black"
shadowBlur={10}
shadowOffset={{ x: 5, y: 5 }}
shadowOpacity={0.5}
/>
{/* Circle without shadow for stroke (optimized) */}
<Circle
ref={circleOptimizedRef}
x={250}
y={100}
radius={50}
fill="red"
stroke="black"
strokeWidth={4}
shadowColor="black"
shadowBlur={10}
shadowOffset={{ x: 5, y: 5 }}
shadowOpacity={0.5}
shadowForStrokeEnabled={false}
/>
{/* Labels */}
<Text
x={50}
y={170}
text="With Stroke Shadow"
fontSize={16}
/>
<Text
x={200}
y={170}
text="Without Stroke Shadow\n(Better Performance)"
fontSize={16}
/>
{/* FPS counter */}
<Text
ref={fpsTextRef}
x={10}
y={10}
text="FPS: 0"
fontSize={16}
/>
</Layer>
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageSize">
<v-layer ref="layerRef">
<!-- Circle with shadow for stroke (default) -->
<v-circle
ref="circleWithShadowRef"
:config="circleWithShadowConfig"
/>
<!-- Circle without shadow for stroke (optimized) -->
<v-circle
ref="circleOptimizedRef"
:config="circleOptimizedConfig"
/>
<!-- Labels -->
<v-text :config="defaultLabelConfig" />
<v-text :config="optimizedLabelConfig" />
<!-- FPS counter -->
<v-text
ref="fpsTextRef"
:config="fpsConfig"
/>
</v-layer>
</v-stage>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Konva from 'konva';
const stageSize = {
width: window.innerWidth,
height: window.innerHeight
};
const circleWithShadowConfig = {
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5
};
const circleOptimizedConfig = {
x: 250,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5,
shadowForStrokeEnabled: false
};
const defaultLabelConfig = {
x: 50,
y: 170,
text: 'With Stroke Shadow',
fontSize: 16
};
const optimizedLabelConfig = {
x: 200,
y: 170,
text: 'Without Stroke Shadow\n(Better Performance)',
fontSize: 16
};
const fpsConfig = ref({
x: 10,
y: 10,
text: 'FPS: 0',
fontSize: 16
});
const layerRef = ref(null);
const circleWithShadowRef = ref(null);
const circleOptimizedRef = ref(null);
const fpsTextRef = ref(null);
let anim = null;
onMounted(() => {
anim = new Konva.Animation((frame) => {
// Rotate circles
circleWithShadowRef.value.getNode().rotation(frame.time * 0.1);
circleOptimizedRef.value.getNode().rotation(frame.time * 0.1);
// Update FPS counter
fpsTextRef.value.getNode().text('FPS: ' + frame.frameRate.toFixed(1));
}, layerRef.value.getNode());
anim.start();
});
onUnmounted(() => {
if (anim) {
anim.stop();
}
});
</script>