兼容性矩阵做废,最常见的原因不是不认真,是无限扩张。
每一加一个浏览器版本,维护成本就翻一倍;每一加一个操作系统,又翻一倍;再乘上三个视口档位,就没有人能在每次发版前跑完了。但表格还躺在文档里,看着很全。
结果是一张谁都不会相信的矩阵。
这一篇讲的是怎么让它停下来:怎么收敛、每一组组合的真实代价是多少、什么时候该停。
实测环境:Windows / Python 3.11.8 / Playwright 1.62.0 / Chromium 151.0.7922.34(Chrome for Testing),本地教学页面 /responsive,不连公网。
引擎安装见 Playwright 浏览器文档,视口与设备模拟见 Playwright 模拟文档。
一、先把膨胀过程算出来
矩阵最典型的写法是四个维度相乘:
text
浏览器品牌 4 种(Chrome / Firefox / Edge / Safari)
× 主要版本 6 个
× 操作系统 3 种(Windows / macOS / iOS)
× 视口 3 档(桌面 / 平板 / 手机)
= 216 组
216 组,维护成本不是 216,是 216 的持续叠加。 每加一个品牌,六个版本 × 三个系统 × 三个视口 = 54 组新组合等着你。
而这 216 组里,绝大多数是零信息量的------你从来没在 iOS 上真的点过某个按钮,却要在每次发版前担心它。
更麻烦的是,矩阵一旦膨胀,它会自己往下滚:有人报了个新设备 → 加一行 → 有人建议加个旧版本 → 再加一行 → 三个月后没人知道哪行是必测、哪行是凑数的。
二、代价账:四个维度不是一个价
不同维度的维护成本差得非常远。
| 维度 | 加一档要付什么 | 相对成本 |
|---|---|---|
| 视口档位 | 改一个数字,重跑 | 最低 |
| 操作系统 | 换机器 / 开虚拟机 / 借测试机 | 中 |
| 浏览器品牌 | 重新下载二进制,重建环境 | 高 |
| 具体版本 | 跟着一升就全部要重测一遍 | 最高 |
版本这一维是真正烧钱的地方。 浏览器一升版,你矩阵里所有带版本号的格子全部作废,要重跑一遍------而重跑不产生新信息,只是维持同一批结论。
先铺最便宜的维度。 视口这一维几乎免费,同一台机器、同一份数据,只改视口参数,就能覆盖「桌面 / 平板 / 手机」三种使用形态。这是性价比最高的投入。
我实测的三个视口数据:
| 视口 | 宽×高 | 表格宽 | 首行高 | 整页截图字节 |
|---|---|---|---|---|
| 桌面 | 1280×800 | 1194 | 36 | 13325 |
| 平板 | 800×600 | 714 | 36 | 11268 |
| 手机 | 375×667 | 289 | 89 | 9541 |
首行高从 36 变成 89 像素,截图字节从 13325 变成 9541。 这就是「同一 URL,只换视口」的完整代价------几乎为零,而且一下覆盖了三种屏幕形态。
跑它只需要一段很短的代码:
python
import asyncio
from playwright.async_api import async_playwright
视口表 = [("桌面", 1280, 800), ("平板", 800, 600), ("手机", 375, 667)]
async def 主():
async with async_playwright() as p:
浏览器 = await p.chromium.launch()
for 名字, 宽, 高 in 视口表:
页面 = await 浏览器.new_page(viewport={"width": 宽, "height": 高})
await 页面.goto("http://127.0.0.1:8000/responsive")
await 页面.wait_for_load_state("networkidle")
表格宽 = await 页面.evaluate(
"() => { const t = document.querySelector('table');"
" return t ? Math.round(t.getBoundingClientRect().width) : -1; }"
)
首行高 = await 页面.evaluate(
"() => { const r = document.querySelector('table tr');"
" return r ? Math.round(r.getBoundingClientRect().height) : -1; }"
)
图 = await 页面.screenshot(full_page=True)
print(名字, 宽, 高, 表格宽, 首行高, len(图))
await 页面.close()
await 浏览器.close()
asyncio.run(主())
其余什么都不动,只改视口。 这样得到的差异才能归因到视口。
三、收敛:两个维度,不要三个
矩阵收敛不是靠「我觉得重要」,是靠两个能查证的东西:
- 用户环境占比------你的真实用户到底用什么。查自己的访问统计,别拍脑袋。
- 业务风险------哪一步挂了真会出事。支付提交挂了要赔钱;报表列排歪了顶多难看。
两条都高 → 必测 ,每次改版都跑。 只有一条高 → 抽查 ,发版前挑一两次。 两条都低 → 明确写「不测」,并且写清理由。
python
def 分级(用户占比, 业务风险):
占比高 = 用户占比 >= 0.2
风险高 = 业务风险 == "高"
if 占比高 and 风险高:
return "必测"
if 占比高 or 风险高:
return "抽查"
return "不测"
for 行 in [("win-chromium-1280", 0.62, "高"),
("win-chromium-375", 0.62, "高"),
("mac-chromium-1280", 0.28, "高"),
("ios-webkit-375", 0.10, "高"),
("win-chromium-help", 0.62, "低")]:
print(行[0], 分级(行[1], 行[2]))
「不测」那一档必须留在文档里。 这是整个收敛动作里最反直觉、也最省事的一步:把「明确不测」写下来并写明理由,下一个人就不会再来问「为什么没测 IE」。省下来的每一次解释,都是真实成本。
四、版本这一维的账:为什么别追每个版本
浏览器品牌的成本高,版本的成本更高,因为它有乘数效应。
假设你矩阵里有 3 个品牌 × 2 个系统 × 3 个视口 = 18 组。现在浏览器统一升一个大版本,这 18 组全部需要重跑------不是为了发现新问题,只是为了重新确认一遍已有的结论。
而且品牌升版往往不是同步的。Chrome 先升,Firefox 后升,Safari 再后升。你会陷入一种最难受的状态:为了追踪某一个品牌的版本节奏,被迫定期重跑整个矩阵。
三个能省钱的做法:
- 只测「当前主流版本 + 上一版本」,再往前的直接标「不测」。
- 版本不写死,写「跟随 XX 品牌主版本自动升级」,并在每次发版记录当次实际跑到的版本号。
- 把版本这一维从矩阵里拿掉,改成一条独立的「浏览器发版后回归」流程,只跑必测那几组。
第 3 条最省。矩阵是覆盖面的工具,版本跟进是时效性的事情,两者混在一起就都会做不好。
五、引擎测试的代价,以及它证明不了什么
Playwright 提供 p.chromium / p.firefox / p.webkit 三个引擎入口,切换写法很简单:
python
import asyncio
from playwright.async_api import async_playwright
async def 按引擎跑(地址, 引擎名):
async with async_playwright() as p:
引擎 = getattr(p, 引擎名)
浏览器 = await 引擎.launch()
页面 = await 浏览器.new_page(viewport={"width": 1280, "height": 800})
await 页面.goto(地址)
print(引擎名, 浏览器.version, await 页面.title())
await 浏览器.close()
asyncio.run(按引擎跑("http://127.0.0.1:8000/responsive", "chromium"))
但这一段我只有 chromium 那一支实际跑过。本机的 Firefox 和 WebKit 二进制没下载,未实测。
这个区分不是吹毛求疵,它直接决定成本怎么估:
- 引擎测试很便宜------加一个引擎就是多下一个二进制、多一份 CI 时间。
- 品牌浏览器和真机验证很贵------真机要设备、要人、要排期。
如果你把两者混为一谈,就会出现两种典型浪费:
- 用引擎测试冒充真机验证------省了钱,但真机上的字体渲染、输入法和系统级弹窗一个都没覆盖。
- 反过来,为了「补齐真机」而无限扩张------本来三条必测路径,最后买了五台设备、开了六个虚拟机矩阵。
我的判断是:引擎层用来发现「引擎之间的行为差异」,真机层只用来确认「最关键的那一两条路径」。 前者便宜所以可以多跑,后者贵所以要克制。
六、AI 的位置:整理,不是裁决
AI 在兼容性这件事上能干的活儿很有限,也很清楚:
- 把格式混乱的 issue 整理成统一格式的差异记录
- 把用户反馈里的设备信息归到矩阵的哪一行
- 基于已有实测数据,给出补测建议
它不能干的两件事:
- 生成「某浏览器用户占比最高」这种数字------没有统计就是编
- 把「引擎跑通了」自动升级成「真实浏览器验证通过」
AI 辅助整理 ≠ 全浏览器验证。 这句必须写进流程文档里,否则半年后看到报告标题写着「跨浏览器兼容性验证通过」,没人知道那其实只跑了一个引擎。
七、什么时候别这么做
这是我最想写清楚的一节。
别做全组合覆盖。 216 组、或者任何接近这个数量级的矩阵,最后都会变成一份从没人跑过的文档。一份没人跑过的兼容性矩阵,比没有矩阵更危险------因为它会让人以为兼容性已经被处理过了。
别追每个浏览器版本。 版本一跟,矩阵维护成本就跟着翻倍,而且不产生新信息。
别把引擎测试写成全浏览器验证。 结论要跟着证据走。实测了哪个引擎就写哪个引擎,其余的写「未实测」。
别在低风险路径上花时间。 帮助文档、登录页的边框宽度、后台列表的默认排序------这些地方出了问题不会有人投诉。
别用「有没有崩」当兼容性标准。 页面不报错不代表能用。按钮被挤出屏幕、日期输入控件行为不同、金额格式变了,用户一样完不成任务。
八、什么时候该停
三个可以收手的信号:
- 必测那几组连续三次全绿,且没有任何待验证格子。 此时的边际收益已经很低了。
- 统计显示某个组合的用户占比掉到了 1% 以下。 把它移到「不测」,不是保留在「必测」里装样子。
- 你发现自己在补矩阵,而不是在修问题。 这是最灵敏的信号------当维护成本开始超过收益,就该停了。
反过来,有一个信号说明你该继续:必测格里出了新的失败,且这个组合的用户占比不低。 这时候跑矩阵是划算的。
小结
按收益从高到低:
- 视口优先------同一台机器改个数字就覆盖三种形态,几乎零成本
- 收敛靠两个维度------用户环境占比 + 业务风险,两条都高才进必测
- 「不测」要写下来------写清理由,这是最省事的一步
- 版本别追------版本跟进和矩阵覆盖分成两件事
- 引擎层多跑,真机层克制------便宜的放开,贵的收着
- AI 只整理不裁决------结论跟着证据走
下一篇是 Day39《Web 性能 + AI Lighthouse 分析》。那里也有同一个问题:指标可以无限优化,但收益会快速衰减------讲怎么算清这笔账。