从远程调试开发的经历谈谈浏览器如何高性能截图
发布时间: 2025-12-12
前言
这些年浏览器一直在发展,但是对于截图而言,说实话,个人觉得并没有一个很好突破性的实现方案,以前很多人用的
html2canvas,到近年出现的snapdom,尽管从原理上来说,snapdom已经是一个比较新的方案,实际使用上来说,
snapdom的性能要比html2canvas好很多,主要是因为snapdom是基于浏览器的新特性实现的,html2canvas性能弱
于snapdom的关键点在于它的重绘原理,他把dom拿出来基于自己的理解绘制到canvas上,性能差不说还会导致静态资源重载,
snapdom的性能的确比它提升了很多,对于普通截图场景来说,已经足够,但是对于我近期正在开发的维护的远程调试
功能来说,需要频繁(大约2秒一次,dom节点约5000个的页面)的截图来说,如何控制截图性能是重要的问题。

在当前条件下,浏览器截图的方案有哪些?
1.原生浏览器API录屏 2.浏览器截图插件 html2canvas、snapdom 3.rrweb录制 4.自行采集dom和样式(类rrweb原理)
直接说结论,首先排除掉1、3,方案1的性能无疑是最好的,3、50ms截图完成,1比1的还原效果,但是体验是最差的,不同浏览器的授权
提示不一样、操作方法有差异,在mac下首次使用还会要求用户关闭整个浏览器应用,设置授权后重新打开,这对用户来说
很困难。

然后把rrweb也ban了,尽管截图效果不错,他的性能在复杂页面也很糟糕,在我5000dom的页面上,全程卡死,他的原理就是把dom、鼠标等事件打包, 有dom变化打包增量数据,然后用这些数据来回放,可以获得和真实场景一样的效果,但就是录制性能很糟糕,复杂页面难以使用。
方案4其实比较有潜力,目前我已经解决了数据量大、传输的问题,但是对于页面变化的增量数据的监听和恢复还没有很好的处理,如果监听整个dom的变化
可能又会陷入rrweb的性能问题,目前只实验了全量dom和鼠标位置以及滚动条的监听,这个方案值得探索。

最后说一下浏览器截图插件方案,仍然是现在截图方案的首选,效果不如上面这些方案,缺点还一大堆,但是综合来看是最好的选择, 就单论性能来说,html2canvas确实是比不上snapdom,而且还会导致静态资源重载,它相对于snapdom的优点就是 对老浏览器兼容性更好,经过多年的检验,用户数多,但是我前面也说了我5000dom页面,2秒截图一次,所以肯定不选html2canvas。
snapdom的性能调优
snapdom和html2canvas的性能优化之所以如此重要, 原因就是它截图的过程中是在主线程中执行的,会阻断页面响应,用户看起来就是卡死,无法操作。
对于snapdom的性能优化,我已经趟过坑之后,直接给大家说哪些手段可以提高snapdom的性能,我在5000节点的页面,截图性能, 从snapdom 的 1100ms 一路优化到900ms,再到560ms,耗时下降48%,因此对于截图插件的性能优化是非常有必要的手段。
优化方案汇总
1.高分屏截取视觉像素 2.图片尺寸压缩 3.OffscreenCanvas离屏画布 4.截图逻辑分片算法(预扫描、截图、生成canvas) 5.识别不可见元素,过滤掉他们
接下来逐个来聊聊.
高分屏截取视觉像素
这是什么意思?用高分辨率屏幕的朋友就知道,4k显示器,在系统缩放之后,可以多个像素点显示一个像素的颜色,来达到视网膜分辨率的效果, 因此你截图之后事实上产生的图片是会比你肉眼看到的大,snapdom支持设置dpr,把他设置为1。
dpr: 1图片尺寸压缩
合理的通过scale、quality参数来设置截图的质量,我是通过如下配置,根据不同的性能情况来使用不同的截图配置的, 其中scale会根据我设置的最大尺寸和实际用户的截图尺寸来计算缩放值,比如用户截图1000x800,我使用如下任何一个配置尺寸都不会超过1000x800, 如果用户截图尺寸1900x1080,而我当前的截图是normal,name这个实际的截图尺寸会是1200x682,通过计算scale就是0.63。
在截图和效果之间找一个平衡点是需要你自行尝试的。
static qualityLevels = {
ultra: { quality: 0.95, maxWidth: 1600, maxHeight: 1200, minPerformance: 90 },
high: { quality: 0.88, maxWidth: 1400, maxHeight: 1050, minPerformance: 70 },
normal: { quality: 0.75, maxWidth: 1200, maxHeight: 900, minPerformance: 50 },
low: { quality: 0.6, maxWidth: 1000, maxHeight: 800, minPerformance: 0 }
};OffscreenCanvas离屏画布
这个其实不是说提高截图性能,而是为了减少阻断用户主线程的时长,将画布的像素计算、绘制指令执行等耗时操作转移到WebWorker线程。 这个其实没啥好说的,就是把截图的内容通过离屏画布绘制到canvas上。
截图逻辑分片算法(预扫描、截图、生成canvas)
这个和上面离屏画布一样,主要是为了解决阻断用户主线程的问题,不是提高性能。他的原理就是通过 window.scheduler.yield() 来让出主线程,
在截图逻辑中多个地方主动让出主线程避免长任务。
识别不可见元素,过滤掉他们
最后不得不说的优化杀手锏, 可谓优化效果立竿见影,我在我的页面中,发现高达85%的节点是不需要截图的,

最核心的难点在于如何准确的识别出不可见元素,标记他们,然后利用snapdom的exclude参数和filter参数
来过滤掉他们,我的识别算法也不复杂,exclude配置一些固定不可见的内容,filter参数是一个函数,我的做法是在截图之前
先对页面dom做预扫描,把所有不可见的元素存起来,然后在filter中快速判断某个元素是否需要截图。
判断元素可见性的方法有很多,但是最核心的还是IntersectionObserver,他可以检测元素是否进入了视口。
const observer = new IntersectionObserver((entries, observer) => {
// entries:交叉状态变化的元素数组(每一项是 IntersectionObserverEntry 对象)
entries.forEach(entry => {
// entry.isIntersecting:布尔值,元素是否进入可视区域(核心判断条件)
if (entry.isIntersecting) {
// 元素可见时执行逻辑(如懒加载图片、统计曝光)
console.log('元素进入视口:', entry.target);
// 可选:观察一次后停止监听(避免重复触发)
observer.unobserve(entry.target);
} else {
// 元素离开视口时执行逻辑(如暂停视频播放)
console.log('元素离开视口:', entry.target);
}
});
}, {
// 配置项(可选)
root: null, // 根容器(默认视口,指定时需是目标元素的父/祖先元素)
rootMargin: '0px', // 根容器的扩展/收缩边距(类似 CSS margin,支持负数),如 "100px 0" 表示视口向外扩展100px
threshold: 0 // 触发回调的交叉比例(0=元素刚进入视口就触发,1=元素完全进入视口才触发,可传数组如 [0, 0.5, 1])
});提前识别不可见元素,然后在filter函数中过滤掉:
static filterHidden(element) {
// 策略1: 优先使用预扫描结果 (最快)
if (ScreenPreview.visibilityManager) {
const selfHidden = ScreenPreview.visibilityManager.hiddenElements.has(element);
// 🔥 关键补丁:即使被标记为隐藏,也要检查是否是脱离父元素的特殊定位元素
// 典型场景:父元素 display:none,但子元素 position:fixed 实际可见 (modal/dropdown)
if (selfHidden) {
try {
const computed = window.getComputedStyle(element);
const position = computed.position;
// fixed/absolute 元素可以脱离隐藏的父元素
if (position === 'fixed' || position === 'absolute') {
// 检查自身是否真的可见
const actuallyVisible =
computed.display !== 'none' &&
computed.visibility !== 'hidden' &&
parseFloat(computed.opacity) > 0;
if (actuallyVisible) {
return true; // 放行该元素
}
}
} catch (e) {
// getComputedStyle 失败,保守处理,维持隐藏状态
if (ScreenPreview.DEBUG_MODE) {
sdkLogger.warn('[filterHidden] ⚠️ getComputedStyle 失败:', e);
}
}
}
return ScreenPreview.visibilityManager.isVisible(element);
}
// 策略2: 降级到快速 inline style 检查
return ScreenPreview._filterHiddenFallback(element);
}需要注意的是,filter 函数的算法决定了你截图的最终效果、有可能导致样式偏差、元素缺失,这些是比较难以避免的问题, 因此只能靠你自己开发的时候逐步去调优,目前我如果不预扫描的话截图时间会增加几百毫秒,但是效果还原度比较高,如果加了 filter之后,效率变好的同时,部分页面截图样式还原度会有一些问题,这是需要你自己做好取舍的。
