返回
canvg Canvas 性能优化:解决内存泄漏与渲染变慢
javascript
2024-12-14 16:20:40
canvg Canvas 性能优化:解决内存泄漏与渲染变慢问题
当使用 canvg 库处理 Canvas 绘图,特别是在频繁编辑和重绘的场景下,可能会遇到性能下降和内存占用不断增加的问题。本文将深入探讨该问题的原因,并提供一系列解决方案,帮助提升 canvg 应用的性能和稳定性。
问题分析
canvg 在每次编辑后,如果没有正确地管理资源,会导致内存占用持续上升。主要原因包括:
- 数据 URL 累积 : 每次绘制后,通过
canvas.toDataURL()
生成的图像数据会以字符串形式存储,并且在多次编辑后不断累积,导致内存占用迅速增加。 - SVG 节点未清理 : SVG 结构不断增大,旧的、未使用的 SVG 节点没有被及时移除,导致 DOM 树膨胀。
- canvg 实例重复创建 : 重复创建 canvg 实例而不复用,也会导致额外的资源消耗。
解决方案
以下是一些解决 canvg 内存泄漏和性能下降问题的有效方法:
1. 限制 Undo/Redo 栈大小
为了实现撤销/重做功能,通常需要保存历史编辑状态。但是,无限地保存历史状态会导致内存无限增长。因此,需要限制 Undo/Redo 栈的大小,只保留最近的若干个编辑状态。
代码示例:
const MAX_UNDO_STACK_SIZE = 5; // 最大保存 5 个编辑状态
let undoStack = [];
function saveState(svgData) {
undoStack.push(svgData);
if (undoStack.length > MAX_UNDO_STACK_SIZE) {
undoStack.shift(); // 移除最早的状态
}
}
function undo() {
if (undoStack.length > 1) { // 保留至少一个状态
undoStack.pop(); // 移除当前状态
let previousState = undoStack[undoStack.length - 1];
loadAndDrawSVG(previousState); // 加载并绘制上一个状态
}
}
function loadAndDrawSVG(svgData){
s.loadXml(ctx, svgData);
s.stop();
s.draw();
}
操作步骤:
- 定义
MAX_UNDO_STACK_SIZE
常量,设置 Undo 栈的最大容量。 - 在每次编辑后,调用
saveState
函数保存当前的 SVG 数据。 saveState
函数会检查 Undo 栈的大小,如果超过最大容量,则移除最早的状态。undo
函数执行撤销操作时,从栈中取出上一个状态并加载绘制。
2. 图像数据优化
避免直接使用 canvas.toDataURL()
来保存图像数据,因为 base64 编码的字符串会占用大量内存。可以考虑以下替代方案:
- 离屏 Canvas : 使用离屏 Canvas 缓存中间结果,避免重复绘制和数据 URL 转换。
- 对象 URL : 使用
URL.createObjectURL()
生成指向 Canvas 数据的对象 URL,用完后通过URL.revokeObjectURL()
释放。
代码示例(对象 URL):
function drawDown() {
inp_xmls = XMLS.serializeToString(svg.node);
// 创建离屏 Canvas
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
const offscreenCtx = offscreenCanvas.getContext('2d');
s.loadXml(offscreenCtx, inp_xmls);
s.stop();
s.draw();
// 使用 对象URL
if(currentObjectURL)
{
URL.revokeObjectURL(currentObjectURL);
}
currentObjectURL = offscreenCanvas.toDataURL();// 获取数据 URL 或使用 toBlob
//currentObjectURL = URL.createObjectURL(blob);
var lowerimg = svg.image(currentObjectURL);
lowerimg.node.onload = function () {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空主画布
ctx.drawImage(lowerimg.node, 0, 0); // 绘制图像到主画布
};
}
let currentObjectURL = null;
//...其他代码
//不再需要图像数据时
function cleanup() {
if (currentObjectURL) {
URL.revokeObjectURL(currentObjectURL);
currentObjectURL = null;
}
}
// 在适当的时机调用 cleanup 函数,例如组件卸载时或页面关闭前
window.addEventListener('beforeunload', cleanup);
操作步骤:
- 使用离屏 Canvas 进行绘制。
- 通过
offscreenCanvas.toDataURL()
或offscreenCanvas.toBlob()
获取图像数据。 - 使用
URL.createObjectURL()
生成对象 URL。 - 在绘制完成后或不再需要该图像时,使用
URL.revokeObjectURL()
释放对象 URL。 - 添加
cleanup
函数并在合适的时候调用,例如在window.onbeforeunload
事件中,确保在页面卸载时释放资源。
3. SVG 节点管理
精细化管理 SVG 节点,及时清理不再需要的节点,避免 DOM 树无限增长。
代码示例:
function drawDown() {
inp_xmls = XMLS.serializeToString(svg.node);
s.loadXml(ctx, inp_xmls);
s.stop();
clearOldSvgNodes(5); // 保留最近的 5 个编辑状态
}
function clearOldSvgNodes(keepCount) {
let children = svg.node.children;
let removeCount = children.length - keepCount;
if(removeCount>0){
for(let i = 0; i < removeCount ; i++){
if(children[1]) // 始终保留第一个元素(背景或其他基础元素)
{
svg.node.removeChild(children[1]);
}
else
{
break;
}
}
}
}
操作步骤:
- 在每次绘制后,调用
clearOldSvgNodes
函数。 clearOldSvgNodes
函数接收一个参数keepCount
,表示需要保留的最新编辑状态数量。- 函数会移除超出
keepCount
数量的旧 SVG 节点。 - 始终保留第一个SVG元素,避免删除背景层或者其他基础元素。
4. canvg 实例复用
避免每次绘制都创建新的 canvg 实例,全局复用一个 canvg 实例即可。
代码示例:
// 全局 canvg 实例
let s = canvg('canvas', '', { ignoreMouse: true, ignoreAnimation: true });
function initializeCanvg(svgData){
if(!s)
{
s = canvg('canvas', svgData, { ignoreMouse: true, ignoreAnimation: true });
}else{
s.loadXml(ctx, svgData);
s.stop();
}
}
function drawDown() {
inp_xmls = XMLS.serializeToString(svg.node);
initializeCanvg(inp_xmls)
s.draw();
}
操作步骤:
- 在全局作用域声明 canvg 实例
s
,并进行初始化配置。 initializeCanvg
函数,负责初始化和更新svg数据。- 在
drawDown
函数中,不再重复创建 canvg 实例,直接调用s.loadXml
和s.draw
方法进行绘制。
结论
通过上述优化措施,可以有效地减少 canvg 应用的内存占用,提升渲染性能,避免因频繁编辑导致的卡顿和崩溃问题。选择合适的解决方案需要根据具体应用场景和需求进行权衡,建议结合多种方法以达到最佳效果。同时,良好的代码结构和资源管理习惯也是保证应用性能的重要因素。