Skip to Content
全部文章实践出真知从远程调试开发的经历谈谈浏览器如何高性能截图

从远程调试开发的经历谈谈浏览器如何高性能截图

发布时间: 2025-12-12

前言

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

在当前条件下,浏览器截图的方案有哪些?

1.原生浏览器API录屏 2.浏览器截图插件 html2canvas、snapdom 3.rrweb录制 4.自行采集dom和样式(类rrweb原理)

直接说结论,首先排除掉1、3,方案1的性能无疑是最好的,3、50ms截图完成,1比1的还原效果,但是体验是最差的,不同浏览器的授权 提示不一样、操作方法有差异,在mac下首次使用还会要求用户关闭整个浏览器应用,设置授权后重新打开,这对用户来说 很困难。 1

然后把rrweb也ban了,尽管截图效果不错,他的性能在复杂页面也很糟糕,在我5000dom的页面上,全程卡死,他的原理就是把dom、鼠标等事件打包, 有dom变化打包增量数据,然后用这些数据来回放,可以获得和真实场景一样的效果,但就是录制性能很糟糕,复杂页面难以使用。

方案4其实比较有潜力,目前我已经解决了数据量大、传输的问题,但是对于页面变化的增量数据的监听和恢复还没有很好的处理,如果监听整个dom的变化 可能又会陷入rrweb的性能问题,目前只实验了全量dom和鼠标位置以及滚动条的监听,这个方案值得探索。 1

最后说一下浏览器截图插件方案,仍然是现在截图方案的首选,效果不如上面这些方案,缺点还一大堆,但是综合来看是最好的选择, 就单论性能来说,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%的节点是不需要截图的, 1

最核心的难点在于如何准确的识别出不可见元素,标记他们,然后利用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之后,效率变好的同时,部分页面截图样式还原度会有一些问题,这是需要你自己做好取舍的。

最后编辑于

hi