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.FastLayer(); 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, FastLayer, 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} > <FastLayer ref={layerRef}> {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} /> ))} </FastLayer> </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-fast-layer ref="layerRef"> <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-fast-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>