snapDOM 为什么这么快?深度解析其性能优势

3,626 阅读3分钟

snapDOM 为什么这么快?深度解析其性能优势

引言

在日常业务开发中,我们经常会用到DOM截图功能。业界也有不少开源的工具,例如,html2canvas。但是大部分DOM截图类的工具也都有一个致命的问题就是慢。还是以html2canvas举例,常规情况下生成一次图片大概耗时1s+,遇到大DOM,可能直接就卡成了假死状态,用户体验极差。

但是snapDOM 以其惊人的性能表现脱颖而出。根据基准测试,snapDOM 相比 html2canvas 快 32-133 倍,相比 modern-screenshot 快 2-93 倍

image.png

本文将深入分析 snapDOM 的技术架构,揭示其性能优势的根本原因。

核心性能优势

1. 技术路径的根本差异

snapDOM:DOM 序列化策略
// snapDOM 的核心流程
1. 深度克隆 DOM2. 内联样式 → 3. 内联资源 → 4. 生成 SVG5. 转换为 data URL
html2canvas:Canvas 重绘策略
// html2canvas 的核心流程
1. 解析 DOM 结构 → 2. 计算样式 → 3.Canvas 上逐像素绘制 → 4. 处理每个元素

关键差异:snapDOM 是"数据转换",html2canvas 是"重新渲染"

2. 算法复杂度对比

snapDOM 的 O(n) 复杂度
// 每个 DOM 节点只处理一次
export function deepClone(node, styleMap, styleCache, nodeMap, compress) {
  // 1. 克隆节点结构 - O(1)
  const clone = node.cloneNode(false);

  // 2. 内联样式 - O(1) 每个节点
  inlineAllStyles(node, clone, styleMap, styleCache, compress);

  // 3. 递归处理子节点 - O(n) 总节点数
  node.childNodes.forEach((child) => {
    const clonedChild = deepClone(
      child,
      styleMap,
      styleCache,
      nodeMap,
      compress
    );
  });
}
html2canvas 的 O(n×m) 复杂度
// 每个 DOM 节点都需要在 Canvas 上重新绘制
// n = DOM 节点数,m = 每个节点的像素数
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    // 计算每个像素的颜色值
    ctx.fillStyle = calculatePixelColor(x, y);
    ctx.fillRect(x, y, 1, 1);
  }
}

关键技术优化

1. 多层缓存机制

样式缓存
const styleCache = new WeakMap();
if (!cache.has(source)) {
  cache.set(source, getStyle(source));
}
图片缓存
const imageCache = new Map();
if (imageCache.has(src)) {
  return Promise.resolve(imageCache.get(src));
}
背景图片缓存
const bgCache = new Map();
if (bgCache.has(encodedUrl)) {
  return `url(${bgCache.get(encodedUrl)})`;
}
CSS 类缓存
const baseCSSCache = new Map();
if (baseCSSCache.has(tagKey)) {
  baseCSS = baseCSSCache.get(tagKey);
}

2. 样式压缩优化

CSS 类生成
if (compress) {
  const keyToClass = generateCSSClasses(styleMap);
  classCSS = Array.from(keyToClass.entries())
    .map(([key, className]) => `.${className}{${key}}`)
    .join("");

  // 应用 CSS 类而不是内联样式
  for (const [node, key] of styleMap.entries()) {
    const className = keyToClass.get(key);
    if (className) node.classList.add(className);
  }
}

优势

  • 相同样式只生成一次 CSS 类
  • 大幅减少 SVG 文件大小
  • 提高后续处理速度

3. 资源处理策略

批量异步处理
// 图片批量处理,避免阻塞
for (let i = 0; i < imgs.length; i += 4) {
  const group = imgs.slice(i, i + 4).map(processImg);
  await Promise.allSettled(group);
}

// 使用 idle 回调避免阻塞主线程
await new Promise((resolve) => {
  idle(
    async () => {
      await inlineImages(clone, options);
      resolve();
    },
    { fast }
  );
});

4. 内存使用优化

WeakMap 避免内存泄漏
const styleCache = new WeakMap(); // 自动垃圾回收
流式处理
const queue = [[source, clone]];
while (queue.length) {
  const [srcNode, cloneNode] = queue.shift();
  // 处理单个节点,避免一次性加载所有数据
}
及时清理
const sandbox = document.getElementById("snapdom-sandbox");
if (sandbox && sandbox.style.position === "absolute") sandbox.remove();

输出格式优势

1. SVG 的文本生成优势

// SVG 是文本格式,生成速度快
const svgString = svgHeader + foString + svgFooter;
dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;

优势

  • SVG 生成是字符串操作,非常快
  • 不需要像素级计算
  • 文件大小通常更小

对比其他dom生成图片的工具,snapDOM是采用的将html转成svg之后绘制在dom上的方式。这样做有两个好处:

  • 可以通过直接内联css的方式来实现样式,而不需要每个dom都去获取computedStyle,降低性能开销
  • 免去了dom创建和渲染的过程

通过svg的foreignObject来引入html,是snapDOM一大很有创新性的技术特点。

2. 矢量输出特性

<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
  <foreignObject width="100%" height="100%">
    <div xmlns="http://www.w3.org/1999/xhtml">
      <!-- HTML 内容 -->
    </div>
  </foreignObject>
</svg>

特殊元素处理优化

1. Canvas 自动转换

if (node.tagName === "CANVAS") {
  const dataURL = node.toDataURL();
  const img = document.createElement("img");
  img.src = dataURL;
  img.width = node.width;
  img.height = node.height;
  return img; // 转换为图片元素
}

2. 伪元素内联

// 将 ::before, ::after 转换为真实 DOM 元素
for (const pseudo of ["::before", "::after", "::first-letter"]) {
  const style = getStyle(source, pseudo);
  if (hasContent || hasBg || hasBgColor) {
    const pseudoEl = document.createElement("span");
    pseudoEl.dataset.snapdomPseudo = pseudo;
    // 应用样式并插入到克隆元素中
  }
}

3. 排除元素处理

if (node.getAttribute("data-capture") === "exclude") {
  const spacer = document.createElement("div");
  spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
  return spacer; // 快速跳过
}

性能数据对比

基准测试结果

场景snapDOM vs html2canvassnapDOM vs modern-screenshot
小元素 (200×100)32.27× 更快6.46× 更快
模态框 (400×300)32.66× 更快7.28× 更快
页面视图 (1200×800)35.29× 更快13.17× 更快
大滚动区域 (2000×1500)68.85× 更快38.23× 更快
超大元素 (4000×2000)133.12× 更快93.31× 更快

有兴趣的可以去snapDOM官网运行benchmark对比zumerlab.github.io/snapdom/

snapDOM 的成功证明了在 Web 开发中,选择正确的技术路径和优化策略的重要性。通过将复杂的 DOM 渲染问题转化为简单的数据序列化问题,snapDOM 实现了性能的质的飞跃,为 DOM 截图领域树立了新的标杆。