查了很长时间的问题
一场看似只是“没有 blur”的样式异常,最后被我一路追到 will-change 。这篇文章记录我昨晚是怎么把它拆开的。
一场从“样式没生效”开始的深夜排查,最后被我拆到宿主链和 will-change 身上。
昨晚我坐在电脑前,盯着 widgets 看了快半个小时,越看越不对劲。
卡片该有的毛玻璃效果没出来。不是那种“完全没写样式”的没出来,而是看着像写了,但又像没生效——你能感觉到它想当玻璃,但它就是差那么一口气。
最气人的是,它不是稳定的“完全没有 blur”。有些角度、有些卡片,似乎又能看到一点,像是被什么东西洗掉了。同一套 widgets 里,视觉上还会出现“这张像玻璃,那张像普通半透明面板”的错位。
我当时的第一个反应当然是去查 backdrop-filter 有没有写、透明度是不是太低、背景是不是被什么东西盖住了。甚至开始怀疑自己是不是又踩了某个浏览器兼容性的坑。
但越看越不对。
这类问题最折磨人的地方就在这里:你以为你在查一个 CSS 属性,实际上你在查一整条渲染链路。
Widgets 这套东西里,真正参与视觉结果的,不只是卡片本体。至少还会牵扯下面几层:
这也是为什么我后来一直逼自己不要凭感觉调参数。因为只要责任层没找准,任何看起来有效的透明度微调,都只是把问题遮住,不是把问题解决。
尤其是毛玻璃这种效果,本来就不是靠一层浅色背景假装高级就能糊弄过去的。它必须同时满足三件事:后景真的被采样到、前景真的有玻璃壳、两者之间的层次关系不能把 blur 再洗掉。任何一个环节错位,最后肉眼看到的都只会是一块“想当玻璃但失败了”的半透明板子。
昨晚我做的最正确的一件事,就是不在真实页面里瞎试了。我把自己按在椅子上,逼自己把问题拆开。
我把排查拆成了四个阶段:
这套拆法看起来很笨,但好处非常大——我终于不用再对着一个“什么都在里面”的复杂页面胡思乱想了,而是可以明确知道,问题究竟是在哪一层第一次出现的。
我在 C0 到 C4 的回加里,盯的不是“哪一张卡更像玻璃”,而是哪一个宿主条件一回来,blur 就开始塌。
这样看以后,问题就没有那么玄学了。它第一次稳定塌掉的节点,不在 widget 自己的内部内容层,也不在 rail 的装饰层,而是在承载 motion 的那层宿主项附近。
然后我开始在 C 阶段内部继续拆,一项一项地移除嫌疑条件:
overflow-y: auto → 还是没出来will-change → 等等,blur 回来了overflow-x: clip → 还在结果非常明确:第一个让 blur 恢复的节点,是移除 will-change。
我当时盯着屏幕看了好几秒,心里想:就这?
will-change: transform, opacity对,就是这行非常常见、也经常被当成“性能优化标配”的样式:
will-change: transform, opacity;这东西平时看起来完全无害,甚至有点“我很专业、我做了性能优化”的味道。可一旦它被常驻挂在承担 blur 采样的真实宿主链上,事情就完全变味了。
我这次碰到的不是理论上的“某些情况下可能有影响”,而是很具体的现象:只要这层 will-change 挂回去,Glass 模式下那层本该成立的毛玻璃就明显被打坏;一旦把它从真实 blur host 链上拿掉,结果立刻开始恢复。
为什么会这样?我后来翻了翻 MDN,大概理解了:backdrop-filter 这东西本来就高度依赖浏览器对合成层、采样区域和绘制顺序的处理。will-change 提前把元素推到某种更激进的合成准备状态以后,原本应该在那条链路上成立的背景采样,被切成了另一种结果。它不一定是“滤镜彻底消失”,但最终会让你看到一个非常像“玻璃没成立”的视觉结果。
(说人话就是:你告诉浏览器“这个元素要动起来了,提前做好准备”,结果它准备得太积极,把玻璃效果给“优化”没了。)
也正因为如此,我后来给自己立了一条更硬的规则:
凡是承担真实 blur 的宿主项,不允许再常驻 will-change: transform, opacity。
这不是说 will-change 永远不能用,而是说它不能再被当成默认安全的糖衣,尤其不能不分青红皂白地挂在玻璃宿主上。如果一定要做性能优化,那就:
到昨晚那一轮收尾时,我确认下来的事情大概有这么几条:
第一,Glass 模式下 widgets 毛玻璃的主故障,已经不是“猜测层面”的问题了,真正的恢复点已经找到,而且可以稳定复现与逆转。
第二,rail 上那种“整列浅色矩形托底”的观感,也不是因为某个神秘的大容器偷偷画了一层背景,而是滚动容器裁切、卡片阴影被吃掉、壳层过实这些因素一起叠出来的假象。这部分是顺便发现的,还没完全收干净。
第三,music 也不是一开始就完美。它只是因为内部层次更多,所以比别的卡更像“成立了”;真正的工作还是得回到统一的壳层和真实宿主链上做校准。
当然,昨晚并不等于一切结束。还有几件事我明确知道不能混为一谈:
也就是说,will-change 的定位解决了“主故障元凶是谁”,但它没有自动替我回答所有视觉和交互问题。
我现在回头看,昨晚最值得记一笔的,不是我最后删掉了哪一行样式,而是我终于又一次被提醒:
复杂前端问题里,最危险的动作往往不是改错,而是改得太快。
如果我昨晚一上来就在 Ticket 和 Glass 两边同时乱拧透明度、阴影、border 和 blur 半径,最后很可能会得到一个“看起来暂时顺眼了”的版本,但我根本不知道自己到底修到了什么。
而这一次我最后能把问题追到 will-change,靠的不是灵感,而是纪律:
我现在越来越觉得,这种排查方式本身,也是工程资产。
因为真正可怕的,从来都不是某个具体的 blur bug,而是我们在没有证据的时候,习惯性地把一切都归结为“再调一点参数试试”。
昨晚至少有一件事我已经能说得很确定:这场“没有毛玻璃”的故障,不是感觉问题,也不是浏览器脾气问题。它确实有元凶,而且元凶已经被我从宿主链里揪出来了。
(接下来该去查 Ticket 模式了,但我得先睡一觉。)
全文完,本文经过ChatGPT的文辞优化。不过只要用过GPT的都能看出来,不是而是满天飞。