返回

canvg Canvas 性能优化:解决内存泄漏与渲染变慢

javascript

canvg Canvas 性能优化:解决内存泄漏与渲染变慢问题

当使用 canvg 库处理 Canvas 绘图,特别是在频繁编辑和重绘的场景下,可能会遇到性能下降和内存占用不断增加的问题。本文将深入探讨该问题的原因,并提供一系列解决方案,帮助提升 canvg 应用的性能和稳定性。

问题分析

canvg 在每次编辑后,如果没有正确地管理资源,会导致内存占用持续上升。主要原因包括:

  1. 数据 URL 累积 : 每次绘制后,通过 canvas.toDataURL() 生成的图像数据会以字符串形式存储,并且在多次编辑后不断累积,导致内存占用迅速增加。
  2. SVG 节点未清理 : SVG 结构不断增大,旧的、未使用的 SVG 节点没有被及时移除,导致 DOM 树膨胀。
  3. 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();
}

操作步骤:

  1. 定义 MAX_UNDO_STACK_SIZE 常量,设置 Undo 栈的最大容量。
  2. 在每次编辑后,调用 saveState 函数保存当前的 SVG 数据。
  3. saveState 函数会检查 Undo 栈的大小,如果超过最大容量,则移除最早的状态。
  4. 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);

操作步骤:

  1. 使用离屏 Canvas 进行绘制。
  2. 通过 offscreenCanvas.toDataURL()offscreenCanvas.toBlob() 获取图像数据。
  3. 使用 URL.createObjectURL() 生成对象 URL。
  4. 在绘制完成后或不再需要该图像时,使用 URL.revokeObjectURL() 释放对象 URL。
  5. 添加 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;
        }
      }
  }

}

操作步骤:

  1. 在每次绘制后,调用 clearOldSvgNodes 函数。
  2. clearOldSvgNodes 函数接收一个参数 keepCount,表示需要保留的最新编辑状态数量。
  3. 函数会移除超出 keepCount 数量的旧 SVG 节点。
  4. 始终保留第一个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();
}

操作步骤:

  1. 在全局作用域声明 canvg 实例 s,并进行初始化配置。
  2. initializeCanvg 函数,负责初始化和更新svg数据。
  3. drawDown 函数中,不再重复创建 canvg 实例,直接调用 s.loadXmls.draw 方法进行绘制。

结论

通过上述优化措施,可以有效地减少 canvg 应用的内存占用,提升渲染性能,避免因频繁编辑导致的卡顿和崩溃问题。选择合适的解决方案需要根据具体应用场景和需求进行权衡,建议结合多种方法以达到最佳效果。同时,良好的代码结构和资源管理习惯也是保证应用性能的重要因素。