实战调试案例用Playwright测量而非盲目假设定位真因,展示了科学调试方法论,避免错误导向(深色模式假设)。
我是韩国济州岛的一名大一学生。我独自开发了一款学习应用——我并不是专业开发者。
在准备面向日本发布时,我遇到了一个 bug:吉祥物图片放在页面上时几乎变成了纯黑色,但直接在浏览器标签页中打开却完全正常。我认定这是 Chrome 强制深色模式造成的,并基于这个假设“修复”了六次。
直到最后用 Playwright 实际测量,我才发现这件事和深色模式毫无关系。无论开启还是关闭该选项,没有一个像素发生变化。
任何在深色主题 UI 中使用透明 PNG 的人
任何正在怀疑 Chrome 强制深色模式的人
任何正在与 AI coding agent 结对编程的人
图片为什么会以一种不容易察觉的方式黑成一片
如何用测量而不是猜测否定一个假设
什么时候应该停止猜测
我在一个深色主题的落地页上放了一只奶油色的兔子吉祥物(透明 PNG)。
在页面上,兔子的身体与背景融为一体,只剩下轮廓和耳朵上的粉色依然可见。直接打开图片:正常。嵌入页面:黑成一片。
Chrome 的“以深色模式显示网页内容”功能会根据渲染尺寸对 <img> 元素进行分类。小图标不会受到影响;大图片则会被当作照片处理,其中的明亮区域会被压暗。
我的 header logo(32px)显示正常,大号吉祥物(160px)却变成了黑色。这个假设与现象完美吻合。
添加 <meta name="color-scheme" content="dark">。没有变化。
同一个功能会通过另一条路径处理 CSS background images,因此它们不会被反色。我把 <img> 换成了 background-image。没有变化。
改用 <svg><image href="..."/></svg>,让它不再被识别为 <img>。没有变化。
此外,我给 AI agent 的指令含糊到让它把“用 SVG 包裹图片”理解成了“参考原图,用 SVG 重新画一张”。结果我得到了一只完全不同的动物。给 Agent 下指令时一定要具体。
直接打开图片后,我发现它的颜色确实已经褪色。去除背景的处理步骤改变了 RGB 值。
我从原图重新制作,只使用 Pillow 修改 alpha channel。替换前逐像素验证 RGB 完全一致。仍然没有变化。
(这项验证存在一个漏洞,后面会讲到。)
从当前元素一路向上检查每一个祖先元素,直到 <html>,查看 filter、mix-blend-mode、backdrop-filter 和 opacity。全部都是 none。
到了这一步,我不再继续猜测。
# Screenshot the full page with forced dark mode ON and OFF, then diff
browser = playwright.chromium.launch(args=[
"--force-dark-mode",
"--enable-features=WebContentsForceDark",
])
max pixel diff across whole page: 0
mean pixel diff: 0.0
differing pixels: 0 / 1,152,000
切换强制深色模式没有造成任何变化。一个像素都没有。
为了做完整性检查,我用相同的 flags 测试了一个没有声明 color-scheme 的纯净测试页面。一个奶油色方块从 (245,240,230) 变成了 (41,37,30)——正确发生了反色。因此 flag 确实在工作,只是根本没有影响我的页面。
我一直追查的嫌疑人,从未出现在案发现场。
源图片的尺寸是 1408×768。我把它渲染进了 header 中一个 32px 的区域,以及 hero 中一个 128px 的区域。缩小比例达到了 11 倍至 44 倍。
图片的透明区域也很大,所以使用 object-contain 后,兔子本身实际渲染出来甚至比 32px 还小。
只有边缘发生了塌缩。图片并没有被反色——而是某种深色在缩小过程中渗进了边缘。
关于这些深色来自哪里,目前有两种可能:
理论 1:浏览器重采样时混入了背景色。缩小过程中,半透明的边缘像素与其后方的深色背景发生混合。但浏览器通常使用 premultiplied alpha 进行重采样,所以我不太相信仅凭这一点就能造成如此严重的塌缩。
理论 2:透明像素原本就是黑色的。经过背景移除工具处理的图片,往往包含大片“完全透明,但 RGB 是黑色”的区域。单独查看时看不出来——但缩小后,黑色就会渗入边缘。
在第 4 步中,我验证了重新制作的图片与原图逐像素完全一致。但因为我的对照对象就是原图,所以如果原图透明区域下的颜色本来就是黑色,这个问题也会原封不动地通过检查。我以为自己验证了某件事,其实什么都没有验证。
我认为理论 2 的可能性更大。验证起来很简单——只需要查看 alpha 为 0 的像素对应的 RGB:
from PIL import Image
import numpy as np
a = np.array(Image.open("mascot.png").convert("RGBA"))
transparent = a[a[..., 3] == 0]
print(transparent[:, :3].mean(axis=0)) # near black → theory 2
如果理论 2 成立,修复方法就是用相邻不透明像素的颜色填充透明区域的 RGB——这通常被称为 colour bleed 或 edge padding。大多数游戏纹理工具都内置了这一功能。
等我完成检查后,会更新这篇文章。如果你以前遇到过这个问题,我很想知道最终是哪一种原因。
正确的修复方法,是按照实际渲染尺寸准备图片。先使用 premultiplied alpha 进行重采样,再执行 unpremultiply。跳过这一步,图片边缘就会变暗。
当时发布期限将近,所以我在吉祥物后面加了一个白色圆形:
<div className="rounded-full bg-white p-2">
<Logomark />
</div>
无论真正的原因是什么,这都消除了深色渗入的可能性。同时,它也提高了吉祥物在深色主题中的可见度,因此从设计角度看,效果也不错。
但这种做法只是消除了症状,并没有证明原因。
我的第一个假设完美解释了所有症状。小 logo 正常、大图片变黑——与强制深色模式基于尺寸进行分类的行为完全吻合。正因为如此,直到第五次尝试,我都没有质疑过这个假设。
真正的机制却是:“渲染区域越大,缩小比例也越大,因此边缘渗色表现得越明显。”原因截然不同,症状却完全一致。
在第 4 步中,我写道自己已经验证 RGB 逐像素一致。数字确实是正确的,但我选错了比较对象,因此什么也没有得知。
验证代码通过时,会给人一种安全感。但这项验证能排除什么、不能排除什么,取决于代码之外的条件。
第 6 步中的 Playwright 测量用了不到 30 分钟。如果一开始就测量,我本可以省下五轮修复。
猜测并修复,只要有效就没什么问题。但连续错两次,就应该开始测量。这才是正确的临界点。
这项工作的大部分内容都是我委托给 AI coding agent 完成的。Agent 不会质疑你的假设,而是会忠实、快速地实现它。因此,一个错误假设会以极高的速度叠加出一连串错误修复。
有一段时间,我已经堆叠了三层 workaround,而它们本身正在成为新的 bug 来源。
让 Agent“测量原因并返回报告”,而不是让它“修复这个问题”,最终反而更快。正是这个做法,在一次尝试中解决了问题。
每次考试前,往年试题总会从某个地方冒出来:可能来自社团前辈、实验室同学,也可能来自你加入的某个群聊。
但这些渠道我一个都没有。同样的课程,同样的学习时间,起跑线却不一样。
这不只是韩国才有的问题。在日本,我不断遇到同样的情况——如果你不属于某个圈子,就拿不到往年试题。国家不同,结构却完全一样。
所以我做了一个不依赖社交网络也能获得帮助的工具:你上传自己的课程材料,它会针对每位授课教师分析内容,生成可能出现的考试题目和一份精简摘要。你的文件始终属于你——不会与其他用户共享,也不存在文档资料库。
carrotly.app——免费,无需注册。
坦白说,我完全不知道它在欧洲或美国的课程体系中表现如何。如果它无法适应你的课程,那正是我最想得到的反馈。
我 logo 里的兔子,就是那只挺过了六轮修复的吉祥物。
感谢阅读。如果你也遇到过同样的症状,希望这篇文章能帮你节省几个小时。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。