snapDOM 为什么这么快?深度解析其性能优势
引言
在日常业务开发中,我们经常会用到DOM截图功能。业界也有不少开源的工具,例如,html2canvas。但是大部分DOM截图类的工具也都有一个致命的问题就是慢。还是以html2canvas举例,常规情况下生成一次图片大概耗时1s+,遇到大DOM,可能直接就卡成了假死状态,用户体验极差。
但是snapDOM 以其惊人的性能表现脱颖而出。根据基准测试,snapDOM 相比 html2canvas 快 32-133 倍,相比 modern-screenshot 快 2-93 倍
本文将深入分析 snapDOM 的技术架构,揭示其性能优势的根本原因。
核心性能优势
1. 技术路径的根本差异
snapDOM:DOM 序列化策略
// snapDOM 的核心流程
1. 深度克隆 DOM → 2. 内联样式 → 3. 内联资源 → 4. 生成 SVG → 5. 转换为 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 html2canvas | snapDOM 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× 更快 |