本来我是想刷一眼那篇 320B 的 agent 论文——arXiv 十月底收的 IQuest-Q1,架构是稀疏 MoE,推理激活 15B,训练还分三个阶段,预训练要用 Stable SplitNorm、Muon 和 muP。结果看到一半我又把它关了,回去钻我那个老 bug 了。嘿嘿,我就这点出息。
现象是这样的:我本意就是给个父元素加 hover 高亮,Chrome 上美滋滋,一上 iOS Safari 17 以下,第一次点下去跟没点一样,要点第二次才亮。你说气不气。我第一次撞见是在一台 iPhone 12 上,Safari 16.6,当时还以为是触屏事件延迟,于是加了个 touchend 模拟点击,结果还是一样。这头牛,我今天非钻穿不可。
我先把最小复现拆出来。代码大概是这个样:
.card:has(.active) {
background: #ffe;
transition: background .2s;
}
Chrome 105 以上点一下亮一下,Firefox 121 以上也顺。就 Safari 17 以下那块地方,首帧直接消失。我第一反应是 :has() 的优先级或者命中有问题——毕竟 :has() 本身就支持晚,Safari 15.4 才给,指不定它对动态命中有什么没说清楚的毛病。
于是我把 :has(.active) 换成写死的 .is-active:
.card.is-active {
background: #ffe;
transition: background .2s;
}
正常。点一下亮一下,首帧一点没丢。所以不是 transition 本身,也不是背景色的事,是 :has() 这个选择器在里面作妖。
那接下来就控制变量嘛。我把 transition 拿掉,只留 :has():
.card:has(.active) {
background: #ffe;
}
也正常,连首帧都有。也就是说,单独的 :has() 没问题,单独的 transition 也没问题,俩拼一块才丢首帧。就离谱。
这时候我拍桌子骂 Safari 是 bug,还去 issue 区贴了条。后来想想别急,先自己再摸两下。我把 transition 的时长从 .2s 改成 0s:
.card:has(.active) {
background: #ffe;
transition: background 0s;
}
不丢帧了。改成 0.001s,又丢。就这地方,我卡了俩小时。
中间我一度以为跟 will-change 有关。想着给它加个 will-change: background 逼它提前层化,说不定能绕开首帧吞掉的问题:
.card:has(.active) {
background: #ffe;
transition: background .2s;
will-change: background;
}
结果更糟,整块背景在 Safari 上闪得跟鬼片一样,还触发了合成层反复重建。哎,删除。接着我试 @supports (selector(:has(*))) 想做兜底,写出来也没用:
@supports (selector(:has(*))) {
/* 想说有 :has() 才用过渡,结果问题根本不是支不支持 */
}
因为问题压根不在支不支持,在于命中的那一刻 transition 把帧吞了,@supports 拦不住那个行为。这一步走完我基本确认,这跟能力检测没关系,是运行时样式重算的时序问题。
等等,这不对劲。我回头把 viewport 拖小又拖大,再把父元素里一个无关的 overflow 去掉,发现规律是:transition 在 :has() 命中导致样式重算的那一帧,把重算的结果直接当成“已经过渡过”了,所以第一帧的背景变化根本没画出来,第二次触发才有旧值可过渡。文档我之前扫过一眼,没往心里去,这就是不细看的报应。我那条 issue 偷偷删了,别笑。不钻到底不舒服斯基。
所以真相不是 bug,是个机制性的坑::has() 命中触发样式重算,transition 会把那一次重算当过渡起点给吃了。你以为是首帧丢了,其实是它把你第一次点下去那一帧当成了“没发生变化”。这跟 layer、跟优先级都没关系,就是重算和过渡的先后顺序撞一块了。
| 浏览器 | 支持 | 备注 |
|---|---|---|
| Chrome | 105+ | 全绿,transition 也顺 |
| Firefox | 121+ | 全绿,没这个丢帧的毛病 |
| Edge | 105+ | 跟 Chrome 一致 |
| Safari | 15.4+ | 15.4-16.6 丢首帧,17 以下最拉胯;17.0 我个人手测没复现 |
拉胯的还是 Safari——你永远可以相信它在交互细节上给你整点阴间活儿。上表里三条是我自己手测的,环境是 iPhone 12 的 Safari 16.6、一台 macOS 上的 Safari 17.0,还有同机的 Edge 105 与 Firefox 121,不敢说覆盖所有设备。移动端要稳,我这边的笨办法就是:可 :has() 的容器上别做首帧样式切换,或者干脆把过渡撤了换 transform 硬刚。
最后,说回那篇 320B 的 agent 论文——它那页作者列表我开完 JavaScript 才看全,一百七十多个名字,PDF 有 2,232 KB。作者说后训练用多脚手架加 SFT 加领域强化学习,还说模型合并能缓解参数冲突,目的是整合互补训练信号、增强 agent 编程能力。听起来挺对,但这种“看起来对”的东西我最近是真不敢直接信了,除非它自己在 iOS Safari 16.6 上点两下。一个 :has() 加 transition 都能整出俩小时,320B 参数的模型写的前端,我只敢信它在我电脑上跑通的那半个浏览器。扯远了,反正——嘿嘿,下次见。
