DOM 比较 vs 视觉比较:两种方法,两个盲区
DOM 比较 vs 视觉比较:两种界面变更检测方法的对比——前者分析 HTML 树(Document Object Model)的修改,后者逐像素比较截图——每种方法都有另一种无法覆盖的盲区。
这是一个你可能经历过的场景:你的团队部署了一个更新。单元测试通过。集成测试通过。端到端测试通过。然而,一个用户报告说支付按钮在手机端消失在了 footer 下面。
这怎么可能?因为你的测试验证了 DOM 包含正确的按钮、正确的文本和正确的链接——但没有人验证这个按钮在屏幕上是否实际可见、位置是否正确、大小是否正确。
这正是在 DOM 比较和视觉比较之间选择所带来的问题。这两种方法经常被当作替代方案。实际上,它们是同一个问题的两个互补面——单独使用其中一个意味着在测试策略中接受一个盲区。
本文详细说明了每种方法能检测到什么、遗漏了什么,以及为什么结构化比较——既读取 DOM 又检查计算后的 CSS 属性——是当今视觉回归问题最完整的答案。
DOM 比较实际做什么
DOM 比较是在时间 T 对页面的 HTML 树取快照,然后与时间 T+1 的快照进行比较。如果节点被添加、删除或修改,diff 会标记它。
这是检测结构变化的强大方法。一个段落被意外删除、一个 href 属性被修改、一个 CSS class 被添加或删除——DOM 比较能看到所有触及文档结构的变化。
使用这种方法的工具众多。Jest snapshot 测试是最广泛使用的例子。你序列化一个 React 或 Vue 组件的渲染结果,存储在文件中,每次运行时 Jest 将当前结果与存储的快照进行比较。
问题在于 DOM 比较只能看到 HTML。它看不到视觉结果。
DOM 比较检测不到什么
让我们看一个具体的例子。你有一个带有 .btn-primary class 的按钮。在你的 CSS 文件中,这个 class 定义了 background-color: #2563EB(蓝色)。一个开发者修改了 CSS 文件并将颜色改为 #DC2626(红色)。HTML 没有任何变化。DOM 完全相同。Jest snapshot 测试通过,显示绿色。
但你的按钮在生产环境中从蓝色变成了红色。
这不是理论情况。以下是 DOM 比较视而不见的具体场景。
外部 CSS 变化。 样式表、主题文件、自定义 CSS 变量、design system token 的任何修改——这些都不会出现在 DOM 中。HTML 保持相同,只有渲染结果变了。而渲染结果才是你的用户看到的。
字体问题。 一个不再加载的 Google Fonts 字体、一个被激活的系统 fallback、一个改变了的 font-weight——DOM 仍然包含相同的 <p> 标签和相同的文本。但在视觉上,你的页面整个排版节奏被破坏了。
z-index 和覆盖问题。 两个元素因 z-index 冲突而重叠、一个 modal 出现在内容下方而不是上方、一个 tooltip 溢出了它的容器——DOM 正确包含了所有元素。错误的是它们的视觉堆叠顺序。
响应式问题。 一个不再正确换行的 flex 容器、一个溢出父元素的元素、一个不再生效的 media query——DOM 是相同的。改变的是布局。
间距和对齐问题。 一个 margin 从 16px 变为 0px、一个 padding 消失了、元素之间的 gap 改变了——如果这些属性在 CSS 中定义,DOM 中什么都看不到。
DOM 比较在设计上就对 HTML 之外定义的一切视而不见。而在现代 Web 应用中,大多数视觉渲染是由 CSS 定义的——不是 HTML。
视觉比较实际做什么
视觉比较从另一端处理问题。它不比较代码,而是比较图像。你在时间 T 捕获页面的截图(baseline),然后在时间 T+1 再捕获一张,算法逐像素比较两张图像——或者使用更复杂的感知方法如 pHash 或 SSIM。
优势显而易见:视觉比较看到的是用户看到的。如果按钮颜色变了,它能检测到。如果文本溢出了容器,它能检测到。如果一个元素消失在另一个元素下面,它能检测到。
这就是 Percy、Applitools、Chromatic 和 BackstopJS 等工具使用的方法。它普及了视觉回归测试的概念,使数千个团队能够检测到功能测试看不到的 Bug。
但它也有自己的盲区。而且这些盲区相当大。
视觉比较检测不到什么
不可见但语义重要的变化。 一个链接的 href 从 /checkout 变为 /cart 不会产生任何视觉变化——链接的文本和样式完全相同。但用户点击后不再到达正确的位置。视觉比较什么都看不到。
无障碍变化。 一个被删除的 aria-label、一个被修改的 role、一张图片上缺失的 alt——在截图中什么都看不到。但对于屏幕阅读器用户来说,你的页面已经变得无法使用。
动态内容变化。 一个价格从 29 变为 290、一个计数器显示错误的数字、一个不再加载的用户名——如果布局保持相同,逐像素比较可能不会将其标记为回归,尤其是在高容差阈值的情况下。
大量误报。 这是纯视觉比较的头号问题。一个闪烁的光标、一个不在同一帧的动画、动态内容(日期、时间、广告)、两次运行之间略有不同的字体渲染——所有这些都会产生不是回归的视觉差异。根据 Google 关于测试可靠性的研究(2016),flaky tests 占 Google 所有测试执行的 1.5%,而渲染差异是视觉测试中 flakiness 的主要原因之一。
缺乏解释能力。 当视觉比较向你显示一个 diff 时,它通过高亮一个区域告诉你"这里有什么东西变了"。但它不告诉你变的是什么。是颜色?大小?位置?内容?你必须自己去调查。在一个有数十个变化的复杂页面上,分类排错就变成了一项全职工作。
真正的问题:两种方法,两个对称的盲区
如果你一路跟下来,你应该看到了这个悖论。
DOM 比较检测 HTML 变化但遗漏视觉变化。视觉比较检测视觉变化但遗漏语义变化。两种方法恰好盲在对方最强的方面。
这个悖论不是巧合。它反映了一个网页的基本二元性:代码(DOM + CSS)产生视觉渲染,但两者之间的关系不是双射的。同一个 DOM 根据应用的 CSS 可以产生非常不同的渲染。而同一个视觉渲染可以由非常不同的 DOM 产生。
因此,在 DOM 比较和视觉比较之间做选择是一个伪命题。问题不是"哪个更好"——问题是"如何覆盖两个维度"。
一些团队试图通过结合两种工具来解决这个问题:用 Jest 做 DOM 快照,用 Percy 或 BackstopJS 做截图。这比什么都不做强,但这也意味着要维护两条流水线、管理两套基线、筛选两个误报来源,而且结果之间没有关联。当 Jest 说"DOM 变了"而 Percy 说"视觉变了"时,没有人告诉你这两个变化是否相关。
结构化比较:读取 DOM 并检查计算后的 CSS
存在第三种方法,它既不满足于只看 DOM,也不满足于只看像素。这就是结构化比较——Delta-QA 选择的方法。
原理如下:Delta-QA 不是比较静态 HTML 树或平面图像,而是读取每个 DOM 元素并获取其计算后的 CSS 属性——即浏览器在解析所有级联、继承、media query 和 CSS 变量后实际应用的样式。
具体来说,对于你页面上的每个元素,Delta-QA 都知道其精确位置、实际尺寸、有效颜色、应用的排版、解析后的 margin 和 padding、计算后的 z-index、不透明度和可见性。不是 CSS 源代码中声明的样式——而是浏览器计算并应用后的样式。
这种方法同时解决了两个盲区。
它检测 CSS 变化。 如果一个 CSS 变量变了并影响了按钮颜色,Delta-QA 能看到——因为它比较的是计算后的 CSS 属性,不是 HTML 源代码。按钮的 background-color 从 rgb(37, 99, 235) 变为 rgb(220, 38, 38)。报告会明确说明这一点。
它检测 DOM 变化。 如果一个元素在 HTML 树中被添加、删除或移动,Delta-QA 能看到——因为它逐个元素遍历 DOM。
它不产生渲染相关的误报。 没有逐像素比较,所以不会因闪烁的光标、不同帧的动画或轻微的字体抗锯齿差异而产生 diff。如果计算后的 CSS 属性相同,就没有 diff。
它解释什么变了。 不是在截图上用红色高亮一个区域,Delta-QA 告诉你:"这个元素的 padding-top 从 16px 变为 8px"或"这个标题的 font-weight 从 700 变为 400"。你确切知道什么变了,在哪个元素上,变成了什么值。
5 遍算法
Delta-QA 不满足于两个 DOM 树之间的简单 diff。它的 5 遍结构化算法有条不紊地进行,以保证结果的准确性。
第一遍使用 CSS 选择器、树位置和文本内容的组合,识别两个页面版本之间的对应元素。第二遍比较每对匹配元素的计算 CSS 属性。第三遍检测被添加和删除的元素。第四遍分析空间关系——相对于邻居移动的元素。第五遍汇总结果并消除噪音——不构成重大回归的微小渲染变化。
结果是一份报告,给你精确的变更列表,按严重程度排序,每项都显示受影响的元素、被修改的属性、变更前和变更后的值。
DOM 比较足够用的场景
让我们坦诚一点:DOM 比较自有其用武之地。如果你的目标是验证组件结构在两次提交之间没有变化——而且只验证结构——Jest snapshot 测试就能正确完成工作。它们速度快、免费、集成在 JavaScript 生态系统中,不需要额外的基础设施。
对于希望组件渲染发生变化时收到提醒的前端开发者来说,它是一个轻量级的安全网。只要你知道这张网只覆盖 HTML——不覆盖 CSS、不覆盖布局、不覆盖最终渲染——它就是你工具箱中的合理工具。
当你把 DOM snapshot 测试当作视觉测试的替代品时,问题就来了。它们不是替代品。它是结构测试,不是外观测试。
视觉比较足够用的场景
截图视觉比较也自有其用武之地。对于几乎没有动态内容的非常静态的页面,它效果很好。对于部署前的快速检查——"首页看起来是否正确"——与基线比较的截图是一个不错的快速指标。
它对于检测特定浏览器的渲染回归也很有用。影响 CSS 渐变渲染的 WebKit Bug 不会被 DOM 或结构化比较检测到——你需要看到浏览器渲染的图像。
但如果你在一个具有动态内容、动画、交互状态或仅仅是定期演进的 CSS 的应用上工作,逐像素比较的误报很快就会成为一个运营问题。根据视觉测试社区的实地反馈,团队平均每天花 30 到 60 分钟筛选截图比较工具的误报。
为什么结构化比较是 2026 年的正确答案
Web 已经进化了。现代应用程序使用 design systems、CSS 变量、组件框架、复杂的响应式布局、动态主题构建。CSS 不再是一个写一次就完事的静态文件——它是一个相互作用的动态规则系统。
在这个背景下,只比较 DOM 而不看 CSS,就像检查建筑的蓝图却不检查墙壁是否在正确的位置。而比较截图却不理解结构,就像看一张建筑的照片却无法分辨是屋顶还是地基移动了。
结构化比较——如 Delta-QA 所实践的——是唯一同时理解结构和渲染的方法。它知道按钮存在(DOM),知道它是蓝色的(计算后的 CSS),知道它宽 200px(计算后的尺寸),知道它距页面顶部 340px(计算后的位置)。
如果这些属性中的任何一个发生变化,它都能检测到。如果没有任何变化,它不会产生误报。就这么简单。
而且由于 Delta-QA 无需代码和云端即可工作,你不需要是开发者就能受益于这种精确度。你安装桌面应用,浏览你的网站,工具完成剩余的工作。在本地运行。不把你的数据发送到任何地方。
常见问题
DOM 比较和视觉比较的根本区别是什么?
DOM 比较分析 HTML 树的修改——构成页面结构的标签、属性和文本。视觉比较逐像素比较截图以检测屏幕上的任何可见变化。前者遗漏 CSS 变化,后者遗漏不可见的语义变化。
DOM 可以在不改变视觉的情况下改变吗?
是的,经常如此。一个被修改的 data-* 属性、一个添加了但没有关联样式的 CSS class、一个添加的 HTML 注释、一个产生相同渲染结果的 DOM 结构重组——所有这些情况都修改了 DOM 但不改变页面的外观。这是 DOM snapshot 测试工具中误报的主要来源。
视觉可以在不改变 DOM 的情况下改变吗?
绝对可以。这甚至是现代应用中最常见的情况。一个 CSS 变量修改、一个外部字体更改、一个 CSS 框架更新、一个因修改 CSS 规则导致的 z-index Bug——所有这些都改变了渲染结果但没有触及 HTML。DOM 比较在结构上不可能检测到这些回归。
什么是结构化比较?它与其他两种方法有什么不同?
结构化比较读取每个 DOM 元素并获取其计算后的 CSS 属性——浏览器实际应用的样式。它因此结合了 DOM 的结构视角和渲染的有效视角,而没有逐像素比较的缺点(误报、缺乏解释能力)。这是 Delta-QA 使用的方法。
Jest snapshot 测试足以检测视觉回归吗?
不够。Jest snapshot 测试比较组件生成的 HTML,而不是它们的外观。它们对于检测意外的结构变化很有用,但看不到 CSS 变化、布局问题、z-index 冲突或排版回归。它们是结构测试,不是视觉测试。
Delta-QA 如何避免视觉比较中常见的误报?
Delta-QA 不比较像素——它比较计算后的 CSS 属性。一个闪烁的光标、一个不在同一帧的动画或轻微的字体抗锯齿差异不会产生 diff,因为底层的 CSS 属性没有变化。只有样式、位置或尺寸的真正变化才会被报告。
使用 Delta-QA 的结构化比较需要是开发者吗?
不需要。Delta-QA 是一个 no-code 工具。你安装桌面应用程序,像平时一样浏览你的网站,工具会自动记录和比较。无需集成 SDK,无需编写脚本,无需配置 CI/CD 流水线。一切都在图形界面中完成。
DOM 比较和视觉比较不是坏工具。它们单独使用时是不完整的工具。结构化比较通过结合每种方法的优势来超越它们——没有一种的误报,也没有另一种的盲区。
如果你正在用 DOM snapshot 或截图测试你的界面,你已经迈出了正确的一步。但如果你想要完整的覆盖——结构、样式和布局——没有噪音、没有复杂性、不把数据发送到云端,结构化比较是下一个合理的步骤。