HTML5 Canvas 图形缓存性能技巧
如果复杂图形包含许多绘制操作,或你正在应用滤镜, 可以通过缓存图形提高性能。缓存图形时,Konva 会将其绘制到 内部 Canvas 缓冲区。之后,Konva 不会每次重新绘制图形, 而会直接使用缓存版本。
此方法特别适用于:
- 包含许多绘制操作的复杂图形
- 带滤镜的图形
- 不经常变化但需要频繁重绘的图形
要缓存图形,请调用 cache() 方法。可以使用 clearCache() 清除缓存。
缓存如何工作?
对图形调用 cache() 方法时,Konva 会:
- 创建内部 Canvas 缓冲区
- 将图形绘制到缓冲区
- 存储缓冲区供以后使用
缓存后,当图形需要显示时,Konva 会使用缓冲区中的缓存版本,而不每次重新绘制。这比反复绘制图形快得多。
指南
- 不要缓存不带滤镜的简单图形。直接渲染可能比使用缓存版本更快。
- 每个缓存节点都会创建多个 Canvas 缓冲区。不要过度使用缓存,否则会占用大量内存。
- 缓存图形组通常比单独缓存每个图形更好。
- 请分别测量启用和禁用缓存时的性能,以了解实际差异。
以下示例展示了使用和不使用缓存时复杂图形的性能差异:
操作说明:
- 单击舞台任意位置以再添加 1000 个圆形
- 切换复选框以启用或禁用缓存
- 查看 FPS 计数器以了解性能差异
- 圆形组会持续旋转
- 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 a group of circles const group = new Konva.Group({ x: stage.width() / 2, y: stage.height() / 2, }); layer.add(group); // Add initial circles const addCircles = (count) => { const radius = 300; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const distance = Math.random() * radius; const x = Math.cos(angle) * distance; const y = Math.sin(angle) * distance; const circle = new Konva.Circle({ x, y, radius: 5 + Math.random() * 10, fill: Konva.Util.getRandomColor(), shadowColor: 'black', shadowBlur: 10, shadowOpacity: 0.5, shadowOffset: { x: 2, y: 2 }, listening: false, }); group.add(circle); } }; // Add initial circles addCircles(5000); // Add FPS counter const fpsText = new Konva.Text({ x: 10, y: 10, text: 'FPS: 0', fontSize: 16, fill: 'white', shadowColor: 'black', shadowBlur: 5, shadowOffset: { x: 1, y: 1 } }); layer.add(fpsText); // Add circle count text const countText = new Konva.Text({ x: 10, y: 40, text: 'Circles: 1000', fontSize: 16, fill: 'white', shadowColor: 'black', shadowBlur: 5, shadowOffset: { x: 1, y: 1 } }); layer.add(countText); // Create animation const anim = new Konva.Animation((frame) => { group.rotation(frame.time * 0.05); // Update FPS counter fpsText.text('FPS: ' + frame.frameRate.toFixed(1)); }, layer); // Add click handler to add more circles stage.on('click', () => { addCircles(1000); countText.text('Circles: ' + group.children.length); }); // Add DOM checkbox const container = stage.container(); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'cache-toggle'; checkbox.style.position = 'absolute'; checkbox.style.top = '70px'; checkbox.style.left = '10px'; checkbox.style.zIndex = '100'; container.appendChild(checkbox); const label = document.createElement('label'); label.htmlFor = 'cache-toggle'; label.textContent = 'Enable Caching'; label.style.position = 'absolute'; label.style.top = '70px'; label.style.left = '30px'; label.style.color = 'white'; label.style.textShadow = '0 0 5px black'; label.style.zIndex = '100'; container.appendChild(label); // Toggle caching checkbox.addEventListener('change', () => { if (checkbox.checked) { group.cache(); } else { group.clearCache(); } }); anim.start();
import { Stage, Layer, Circle, Text, Group } from 'react-konva'; import { useEffect, useRef, useState } from 'react'; import Konva from 'konva'; const App = () => { const [circles, setCircles] = useState([]); const [isCached, setIsCached] = useState(false); const fpsTextRef = useRef(null); const groupRef = useRef(null); useEffect(() => { // Add initial circles addCircles(5000); // Setup animation const anim = new Konva.Animation((frame) => { if (groupRef.current) { groupRef.current.rotation(frame.time * 0.05); } // Update FPS counter fpsTextRef.current.text('FPS: ' + frame.frameRate.toFixed(1)); }, fpsTextRef.current.getLayer()); anim.start(); return () => anim.stop(); }, []); // Toggle caching useEffect(() => { if (groupRef.current) { if (isCached) { groupRef.current.cache(); } else { groupRef.current.clearCache(); } } }, [isCached]); // Add circles const addCircles = (count) => { const newCircles = []; const radius = 300; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const distance = Math.random() * radius; const x = Math.cos(angle) * distance; const y = Math.sin(angle) * distance; newCircles.push({ id: circles.length + i, x, y, radius: 5 + Math.random() * 10, fill: Konva.Util.getRandomColor(), shadowColor: 'black', shadowBlur: 10, shadowOpacity: 0.5, shadowOffset: { x: 2, y: 2 }, listening: false }); } setCircles(prev => [...prev, ...newCircles]); }; return ( <> <Stage width={window.innerWidth} height={window.innerHeight} onClick={() => addCircles(1000)} > <Layer> <Group ref={groupRef} x={window.innerWidth / 2} y={window.innerHeight / 2} > {circles.map((circle) => ( <Circle key={circle.id} {...circle} /> ))} </Group> <Text ref={fpsTextRef} x={10} y={10} text="FPS: 0" fontSize={16} fill="white" shadowColor="black" shadowBlur={5} shadowOffset={{ x: 1, y: 1 }} /> <Text x={10} y={40} text={`Circles: ${circles.length}`} fontSize={16} fill="white" shadowColor="black" shadowBlur={5} shadowOffset={{ x: 1, y: 1 }} /> </Layer> </Stage> <div style={{ position: 'absolute', top: '70px', left: '10px', zIndex: 100 }}> <input type="checkbox" id="cache-toggle" checked={isCached} onChange={(e) => setIsCached(e.target.checked)} /> <label htmlFor="cache-toggle" style={{ color: 'white', textShadow: '0 0 5px black', marginLeft: '10px' }} > Enable Caching </label> </div> </> ); }; export default App;
<template> <v-stage :config="stageSize" @click="addCircles(1000)" > <v-layer ref="layerRef"> <v-group ref="groupRef" :config="{ x: stageSize.width / 2, y: stageSize.height / 2 }" > <v-circle v-for="circle in circles" :key="circle.id" :config="circle" /> </v-group> <v-text ref="fpsTextRef" :config="fpsConfig" /> <v-text :config="{ x: 10, y: 40, text: `Circles: ${circles.length}`, fontSize: 16, fill: 'white', shadowColor: 'black', shadowBlur: 5, shadowOffset: { x: 1, y: 1 } }" /> </v-layer> </v-stage> <div style="position: absolute; top: 70px; left: 10px; z-index: 100"> <input type="checkbox" id="cache-toggle" v-model="isCached" /> <label for="cache-toggle" style="color: white; text-shadow: 0 0 5px black; margin-left: 10px" > Enable Caching </label> </div> </template> <script setup> import { ref, onMounted, onUnmounted, watch } from 'vue'; import Konva from 'konva'; const stageSize = { width: window.innerWidth, height: window.innerHeight }; const isCached = ref(false); const layerRef = ref(null); const fpsTextRef = ref(null); const groupRef = ref(null); const circles = ref([]); // Add circles const addCircles = (count) => { console.log('addCircles', count); const radius = 300; const newCircles = []; for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const distance = Math.random() * radius; const x = Math.cos(angle) * distance; const y = Math.sin(angle) * distance; newCircles.push({ id: (circles.value.length + i).toString(), x, y, radius: 5 + Math.random() * 10, fill: Konva.Util.getRandomColor(), shadowColor: 'black', shadowBlur: 10, shadowOpacity: 0.5, shadowOffset: { x: 2, y: 2 }, listening: false }); } circles.value = [...circles.value, ...newCircles]; }; // Toggle caching watch(isCached, (value) => { if (groupRef.value) { if (value) { groupRef.value.getNode().cache(); } else { groupRef.value.getNode().clearCache(); } } }); const fpsConfig = ref({ x: 10, y: 10, text: 'FPS: 0', fontSize: 16, fill: 'white', shadowColor: 'black', shadowBlur: 5, shadowOffset: { x: 1, y: 1 } }); let anim = null; onMounted(() => { // Add initial circles addCircles(5000); anim = new Konva.Animation((frame) => { if (groupRef.value) { groupRef.value.getNode().rotation(frame.time * 0.05); } // Update FPS counter fpsTextRef.value.getNode().text('FPS: ' + frame.frameRate.toFixed(1)); }, layerRef.value.getNode()); anim.start(); }); onUnmounted(() => { if (anim) { anim.stop(); } }); </script>