Jumping Bunnies Performance Stress Test
Performance stress test with bouncing bunnies
This demo showcases the performance of moving many Konva.Image objects at the same time. It's adapted from the Bunnymark demo of the PixiJS framework.
Note: You may notice that the Konva version is slower than the original PixiJS version. This is because PixiJS is highly optimized for WebGL rendering and this specific type of animation. While Konva continues to optimize its internals, remember that this demo doesn't represent the performance of typical applications made with Konva.
For applications with a very large number of animated objects, you might consider using Native Canvas Access or even a different framework. Choose the right tool for your specific application needs.
Instructions: Click or touch the canvas to add more bunnies. The counter will show how many bunnies are currently animating.
- Vanilla
- React
- Vue
import Konva from 'konva';
// Set up stage and layer
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer({ listening: false });
stage.add(layer);
// Create stats and counter display
const counterDiv = document.createElement('div');
counterDiv.style.position = 'absolute';
counterDiv.style.top = '50px';
counterDiv.style.backgroundColor = 'white';
counterDiv.style.fontSize = '12px';
counterDiv.style.padding = '5px';
counterDiv.innerHTML = '0 BUNNIES';
document.getElementById('container').appendChild(counterDiv);
// Define variables
const bunnys = [];
const GRAVITY = 0.75;
const maxX = width;
const minX = 0;
const maxY = height;
const minY = 0;
const startBunnyCount = 100; // Starting with fewer bunnies for better initial performance
const amount = 10; // Add this many bunnies at a time
let isAdding = false;
let count = 0;
let wabbitTexture;
// Load the bunny image
wabbitTexture = new Image();
wabbitTexture.onload = function() {
addBunnies(startBunnyCount);
counterDiv.innerHTML = startBunnyCount + ' BUNNIES';
count = startBunnyCount;
// Start animation loop
requestAnimationFrame(update);
};
wabbitTexture.src = 'https://konvajs.org/assets/bunny.png';
// Add event listeners
stage.on('mousedown touchstart', function() {
isAdding = true;
});
stage.on('mouseup touchend', function() {
isAdding = false;
});
// Function to add bunnies
function addBunnies(num) {
for (let i = 0; i < num; i++) {
const bunny = new Konva.Image({
image: wabbitTexture,
transformsEnabled: 'position',
perfectDrawEnabled: false,
x: Math.random() * width,
y: Math.random() * height,
});
bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;
bunnys.push(bunny);
layer.add(bunny);
}
}
// Animation update function
function update() {
// Add more bunnies if mouse is down
if (isAdding) {
addBunnies(amount);
count += amount;
counterDiv.innerHTML = count + ' BUNNIES';
}
// Update all bunnies
for (let i = 0; i < bunnys.length; i++) {
const bunny = bunnys[i];
let x = bunny.x();
let y = bunny.y();
x += bunny.speedX;
y += bunny.speedY;
bunny.speedY += GRAVITY;
// Bounce off the edges
if (x > maxX - wabbitTexture.width) {
bunny.speedX *= -1;
x = maxX - wabbitTexture.width;
} else if (x < minX) {
bunny.speedX *= -1;
x = minX;
}
if (y > maxY - wabbitTexture.height) {
bunny.speedY *= -0.85;
y = maxY - wabbitTexture.height;
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (y < minY) {
bunny.speedY = 0;
y = minY;
}
bunny.position({ x, y });
}
layer.batchDraw();
requestAnimationFrame(update);
}
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
const BunnyMark = () => {
// Constants
const width = window.innerWidth;
const height = window.innerHeight;
const GRAVITY = 0.75;
const START_COUNT = 100;
const ADD_AMOUNT = 10;
// State and refs
const [count, setCount] = useState(0);
const [isAdding, setIsAdding] = useState(false);
const layerRef = useRef(null);
const bunniesRef = useRef([]);
const bunnyNodesRef = useRef([]); // Store references to the actual Konva nodes
const [bunnyImage] = useImage('https://konvajs.org/assets/bunny.png');
// Create a bunny with position and velocity
const createBunny = (x, y) => ({
x,
y,
speedX: Math.random() * 10,
speedY: Math.random() * 10 - 5
});
// Store references to Konva image nodes
const storeNodeRef = (index, node) => {
if (node) {
bunnyNodesRef.current[index] = node;
}
};
// Initialize bunnies when image loads
useEffect(() => {
if (!bunnyImage) return;
const initialBunnies = Array(START_COUNT).fill(0).map(() => createBunny(
Math.random() * width,
Math.random() * height
));
bunniesRef.current = initialBunnies;
bunnyNodesRef.current = new Array(START_COUNT);
setCount(START_COUNT);
}, [bunnyImage]);
// Animation loop
useEffect(() => {
if (!bunnyImage) return;
let animationFrameId;
const update = () => {
// Add more bunnies if needed
if (isAdding) {
const currentLength = bunniesRef.current.length;
const newBunnies = Array(ADD_AMOUNT).fill(0).map(() =>
createBunny(
Math.random() * width,
Math.random() * height
)
);
bunniesRef.current = [...bunniesRef.current, ...newBunnies];
// Extend the nodes array to accommodate new bunnies
bunnyNodesRef.current = [...bunnyNodesRef.current, ...new Array(ADD_AMOUNT)];
setCount(prevCount => prevCount + ADD_AMOUNT);
}
// Update all bunnies - DIRECT NODE MANIPULATION FOR PERFORMANCE
// This avoids expensive React re-renders for position updates
bunniesRef.current.forEach((bunny, i) => {
// Update data model
bunny.x += bunny.speedX;
bunny.y += bunny.speedY;
bunny.speedY += GRAVITY;
// Bounce off edges
if (bunny.x > width - bunnyImage.width) {
bunny.speedX *= -1;
bunny.x = width - bunnyImage.width;
} else if (bunny.x < 0) {
bunny.speedX *= -1;
bunny.x = 0;
}
if (bunny.y > height - bunnyImage.height) {
bunny.speedY *= -0.85;
bunny.y = height - bunnyImage.height;
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (bunny.y < 0) {
bunny.speedY = 0;
bunny.y = 0;
}
// Direct node update if we have a reference (much faster than React updates)
const node = bunnyNodesRef.current[i];
if (node) {
node.x(bunny.x);
node.y(bunny.y);
}
});
// Batch draw the layer once instead of updating each node individually
if (layerRef.current) {
layerRef.current.getLayer().batchDraw();
}
animationFrameId = requestAnimationFrame(update);
};
update();
return () => {
cancelAnimationFrame(animationFrameId);
};
}, [isAdding, bunnyImage]);
// Handle mouse/touch events
const handleDown = () => setIsAdding(true);
const handleUp = () => setIsAdding(false);
if (!bunnyImage) return <div>Loading bunny image...</div>;
return (
<>
<Stage
width={width}
height={height}
onMouseDown={handleDown}
onMouseUp={handleUp}
onTouchStart={handleDown}
onTouchEnd={handleUp}
>
<Layer ref={layerRef} listening={false}>
{bunniesRef.current.map((bunny, i) => (
<Image
key={i}
ref={(node) => storeNodeRef(i, node)}
image={bunnyImage}
x={bunny.x}
y={bunny.y}
transformsEnabled="position"
perfectDrawEnabled={false}
/>
))}
</Layer>
</Stage>
<div
style={{
position: 'absolute',
top: '50px',
backgroundColor: 'white',
fontSize: '12px',
padding: '5px'
}}
>
{count} BUNNIES
</div>
</>
);
};
export default BunnyMark;
<template>
<div>
<v-stage
:config="stageConfig"
@mousedown="isAdding = true"
@mouseup="isAdding = false"
@touchstart="isAdding = true"
@touchend="isAdding = false"
>
<v-layer ref="layerRef" :config="{ listening: false }">
<v-image
v-for="(bunny, index) in bunnies"
:key="index"
:config="{
image: bunnyImage,
x: bunny.x,
y: bunny.y,
transformsEnabled: 'position',
perfectDrawEnabled: false
}"
:ref="el => storeNodeRef(index, el)"
/>
</v-layer>
</v-stage>
<div
style="position: absolute; top: 50px; background-color: white; font-size: 12px; padding: 5px;"
>
{{ count }} BUNNIES
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
// Stage setup with fixed dimensions
const stageConfig = {
width: window.innerWidth,
height: window.innerHeight
};
// Refs
const layerRef = ref(null);
const bunnyImage = ref(null);
const bunnies = ref([]);
const count = ref(0);
const isAdding = ref(false);
const animationFrameId = ref(null);
const bunnyNodesMap = ref({}); // Store references to Konva nodes by index
// Constants
const GRAVITY = 0.75;
const START_COUNT = 100;
const ADD_AMOUNT = 10;
// Store references to Konva image nodes
const storeNodeRef = (index, el) => {
if (el) {
bunnyNodesMap.value[index] = el;
}
};
// Create a bunny with position and velocity
const createBunny = (x, y) => ({
x,
y,
speedX: Math.random() * 10,
speedY: Math.random() * 10 - 5
});
// Animation update function - Using direct node manipulation for performance
const update = () => {
// Add more bunnies if needed
if (isAdding.value) {
const newBunnies = Array(ADD_AMOUNT)
.fill(0)
.map(() =>
createBunny(
Math.random() * stageConfig.width,
Math.random() * stageConfig.height
)
);
// Update the reactive array only once for the new bunnies
bunnies.value.push(...newBunnies);
count.value += ADD_AMOUNT;
}
// Update all bunnies - DIRECT NODE MANIPULATION FOR PERFORMANCE
// This avoids expensive Vue reactivity for position updates
bunnies.value.forEach((bunny, index) => {
// Update data model
bunny.x += bunny.speedX;
bunny.y += bunny.speedY;
bunny.speedY += GRAVITY;
// Bounce off edges
if (bunny.x > stageConfig.width - bunnyImage.value.width) {
bunny.speedX *= -1;
bunny.x = stageConfig.width - bunnyImage.value.width;
} else if (bunny.x < 0) {
bunny.speedX *= -1;
bunny.x = 0;
}
if (bunny.y > stageConfig.height - bunnyImage.value.height) {
bunny.speedY *= -0.85;
bunny.y = stageConfig.height - bunnyImage.value.height;
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (bunny.y < 0) {
bunny.speedY = 0;
bunny.y = 0;
}
// Direct node update if we have a reference (much faster than Vue reactivity)
const node = bunnyNodesMap.value[index];
if (node) {
// Get the Konva node and update position directly
const konvaNode = node.getNode();
if (konvaNode) {
konvaNode.x(bunny.x);
konvaNode.y(bunny.y);
}
}
});
// Batch draw the layer once instead of updating each node individually
if (layerRef.value) {
layerRef.value.getNode().batchDraw();
}
animationFrameId.value = requestAnimationFrame(update);
};
// Load the bunny image
onMounted(() => {
const img = new Image();
img.src = 'https://konvajs.org/assets/bunny.png';
img.onload = () => {
bunnyImage.value = img;
// Add initial bunnies
const initialBunnies = Array(START_COUNT)
.fill(0)
.map(() =>
createBunny(
Math.random() * stageConfig.width,
Math.random() * stageConfig.height
)
);
bunnies.value = initialBunnies;
count.value = START_COUNT;
// Start animation
animationFrameId.value = requestAnimationFrame(update);
};
onUnmounted(() => {
if (animationFrameId.value) {
cancelAnimationFrame(animationFrameId.value);
}
});
});
</script>