HTML5 Canvas 形状缓存性能提示
如果您有一个复杂的形状,其中包含许多绘图操作,或者如果您正在应用过滤器,您可以通过缓存形状来提高性能。当您缓存一个形状时,Konva 会将其绘制到一个内部画布缓冲区。之后,Konva 将简单地使用缓存的版本,而不是每次都重绘该形状。
这在以下情况下特别有用:
- 具有许多绘图操作的复杂形状
- 带有过滤器的形状
- 不经常变化但需要频繁重新绘制的形状
要缓存形状,只需 调用 cache()
方法。您可以使用 clearCache()
清除缓存。
缓存是如何工作的?
当您在形状上调用 cache()
方法时,Konva:
- 创建一个内部画布缓冲区
- 将形状绘制到该缓冲区
- 将该缓冲区存储以供将来使用
在缓存之后,而不是每次需要显示该形状时都进行重绘,Konva 将简单地使用缓冲区中的缓存版本。这比重复重绘形状要快得多。
指南
- 不要缓存没有过滤器的简单形状。直接渲染它可能会更快,而不是从缓存版本中渲染。
- 每个缓存节点会创建几个画布缓冲区。因此不要过度使用,因为这会消耗大量内存。
- 更好地缓存形状组,而不是单独缓存每个形状。
- 记得始终测量带缓存和不带缓存的性能,以查看实际差异。
下面是一个演示,显示了缓存和非缓存复杂形状之间的性能差异:
说明:
- 点击舞台上的任何地方添加 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); // 创建一组圆圈 const group = new Konva.Group({ x: stage.width() / 2, y: stage.height() / 2, }); layer.add(group); // 添加初始圆圈 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); } }; // 添加初始圆圈 addCircles(5000); // 添加 FPS 计数器 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); // 添加圆圈数量文本 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); // 创建动画 const anim = new Konva.Animation((frame) => { group.rotation(frame.time * 0.05); // 更新 FPS 计数器 fpsText.text('FPS: ' + frame.frameRate.toFixed(1)); }, layer); // 添加点击处理程序以添加更多圆圈 stage.on('click', () => { addCircles(1000); countText.text('Circles: ' + group.children.length); }); // 添加 DOM 复选框 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 = '启用缓存'; 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); // 切换缓存 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(() => { // 添加初始圆圈 addCircles(5000); // 设置动画 const anim = new Konva.Animation((frame) => { if (groupRef.current) { groupRef.current.rotation(frame.time * 0.05); } // 更新 FPS 计数器 fpsTextRef.current.text('FPS: ' + frame.frameRate.toFixed(1)); }, fpsTextRef.current.getLayer()); anim.start(); return () => anim.stop(); }, []); // 切换缓存 useEffect(() => { if (groupRef.current) { if (isCached) { groupRef.current.cache(); } else { groupRef.current.clearCache(); } } }, [isCached]); // 添加圆圈 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' }} > 启用缓存 </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" > 启用缓存 </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([]); // 添加圆圈 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]; }; // 切换缓存 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(() => { // 添加初始圆圈 addCircles(5000); anim = new Konva.Animation((frame) => { if (groupRef.value) { groupRef.value.getNode().rotation(frame.time * 0.05); } // 更新 FPS 计数器 fpsTextRef.value.getNode().text('FPS: ' + frame.frameRate.toFixed(1)); }, layerRef.value.getNode()); anim.start(); }); onUnmounted(() => { if (anim) { anim.stop(); } }); </script>