Web 兼容性 + AI 跨浏览器

兼容性矩阵做废,最常见的原因不是不认真,是无限扩张。

每一加一个浏览器版本,维护成本就翻一倍;每一加一个操作系统,又翻一倍;再乘上三个视口档位,就没有人能在每次发版前跑完了。但表格还躺在文档里,看着很全。

结果是一张谁都不会相信的矩阵。

这一篇讲的是怎么让它停下来:怎么收敛、每一组组合的真实代价是多少、什么时候该停。

实测环境: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 再后升。你会陷入一种最难受的状态:为了追踪某一个品牌的版本节奏,被迫定期重跑整个矩阵。

三个能省钱的做法:

  1. 只测「当前主流版本 + 上一版本」,再往前的直接标「不测」。
  2. 版本不写死,写「跟随 XX 品牌主版本自动升级」,并在每次发版记录当次实际跑到的版本号。
  3. 把版本这一维从矩阵里拿掉,改成一条独立的「浏览器发版后回归」流程,只跑必测那几组。

第 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. 必测那几组连续三次全绿,且没有任何待验证格子。 此时的边际收益已经很低了。
  2. 统计显示某个组合的用户占比掉到了 1% 以下。 把它移到「不测」,不是保留在「必测」里装样子。
  3. 你发现自己在补矩阵,而不是在修问题。 这是最灵敏的信号------当维护成本开始超过收益,就该停了。

反过来,有一个信号说明你该继续:必测格里出了新的失败,且这个组合的用户占比不低。 这时候跑矩阵是划算的。

小结

按收益从高到低:

  1. 视口优先------同一台机器改个数字就覆盖三种形态,几乎零成本
  2. 收敛靠两个维度------用户环境占比 + 业务风险,两条都高才进必测
  3. 「不测」要写下来------写清理由,这是最省事的一步
  4. 版本别追------版本跟进和矩阵覆盖分成两件事
  5. 引擎层多跑,真机层克制------便宜的放开,贵的收着
  6. AI 只整理不裁决------结论跟着证据走

下一篇是 Day39《Web 性能 + AI Lighthouse 分析》。那里也有同一个问题:指标可以无限优化,但收益会快速衰减------讲怎么算清这笔账。

相关推荐
陈随易5 小时前
Rolldown 把插件改动回传提速 26 倍
前端·后端·程序员
陈随易14 小时前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员
皮皮林55114 小时前
40亿个QQ号,限制1G内存,如何去重?
程序员
程序员cxuan19 小时前
LobeHub 是真的夯,简直 Super-Agent 最佳实现。
人工智能·后端·程序员
AI前端工程狮21 小时前
1 秒超时,3 秒才写完:MCP 写操作的重复提交实测
人工智能·程序员
KylinLab1 天前
TRAE Bot 上手实测:任务级模型固定 DeepSeek-V4.1-Flash,九步流程只留两步工件
程序员
李剑一1 天前
她是世界上第一位程序员,写下了人类第一行代码,但却因偏执的信任数学计算而早逝
程序员
陈随易1 天前
神级 Skill,让 AI 用 HTML 代替 Markdown 回复
前端·后端·程序员
尼克陈1 天前
自媒体平台账号多登-辛小蜂
程序员