跳到主要内容
一个 900px 的块是怎么被压进 375px 的屏幕里的

一个 900px 的块是怎么被压进 375px 的屏幕里的

陈渊
陈渊

· 阅读约 6 分钟

那篇论文说大多数模型生成的网页里都塞了响应式设计模式,但这些模式没被正确实现。初看像一句废话——模型连 @media (max-width: 600px) 都能吐出来了,怎么叫没实现对?往下挖一层才看得见,真正坏掉的环节往往不在查询本身,而在比它更前面的一道设置:视口。

这一层大多数人写前端的时候根本没碰过,因为框架把默认值替你填了。模型倒是把一堆 @mediaflex-direction: column 抄得挺像,偏偏那道 3 行能改变的设置——没了,或者猜错了。这篇我们动手实现一个最小可运行版本的视口选择,追一遍当一个 900px 的块被丢进 375px 的手机屏幕时,机器到底在干什么。

移动浏览器在布局引擎开始计算每个 <div> 多宽之前,必须先回答一个前置问题:画布多宽?这个宽度不叫屏幕宽度,叫布局视口。没有显式设置时,移动浏览器会给一个桌面尺寸的默认值,iPhone 上长期是 980px。

先写一个最朴素的模型:

SCREEN_PHYSICAL_W = 375
DEFAULT_LAYOUT_W = 980

def layout_viewport(meta_present: bool) -> int:
    return SCREEN_PHYSICAL_W if meta_present else DEFAULT_LAYOUT_W

def scale_factor(layout_w: int) -> float:
    return SCREEN_PHYSICAL_W / layout_w

def physical_width(css_width: int, layout_w: int) -> float:
    return css_width * scale_factor(layout_w)

三行都是定义,真正跑起来才见血。我们往里面丢一个 900px 的块:

layout_w = layout_viewport(meta_present=False)
print(layout_w)                        # 980
print(scale_factor(layout_w))          # 0.3826530612244898
print(physical_width(900, layout_w))   # 344.3877551020408

这个块在手机屏幕上看起来是 344px 宽,不是它声明的 900px。浏览器把整个 980px 的画布缩到 375px 来显示,所以页面整体看起来像从远处拍的一张桌面端截图。再追一个字体的物理尺寸:

print(physical_width(16, layout_w))    # 6.122448979591836

16px 的文字最后落在视网膜屏幕上只有 6.1px 的物理宽。能多物理?6px 的字大概就是人眼贴到屏幕上也得眯一下的程度。这就是那 88.3% 布局级失效里最典型的一种:整个页面缩小不是为了适配,而是因为布局视口选错了之后被等比压缩。字体过小的根因不在 font-size 写小了,而在这道画布该多宽的前置判断。

把 meta 视口补上,情况立刻反转:

layout_w = layout_viewport(meta_present=True)  # 375
print(physical_width(900, layout_w))           # 900.0

同一个 900px 的块,现在物理宽度也是 900px。问题是屏幕只有 375px。它不会缩小,而是直挺挺溢出屏幕,右边被截断。内容被截断这种症状和前面那个缩小看着是两码事,其实共享同一条根——一个 900px 的块,画布宽度先被定成了 980 还是 375,决定了它最终是缩成一小坨,还是冲出右边。

到这一步就清楚了:所谓响应式,第一步根本不是媒体查询,而是布局视口宽度的选择。这一步决定后面所有 CSS 里的比例和断点到底在跟谁算账。

往下追媒体查询。我一开始也以为是模型没理解断点逻辑,翻完这个机制才发现更难受的一点——媒体查询判断的是布局视口宽度,不是屏幕物理宽度。我们把检测函数写出来跑一遍:

def hits_mobile_breakpoint(layout_w: int, breakpoint: int = 600) -> bool:
    return layout_w <= breakpoint

没有 meta 视口的时候:

print(hits_mobile_breakpoint(layout_viewport(meta_present=False)))  # False

你写的 @media (max-width: 600px) 一个字错都没有,语法也挑不出毛病,但它在那个 375px 宽的手机上永远不会命中。因为布局视口仍然是 980。模型不是不会写查询,是它没意识到这套规则跑在一个错误尺寸的画布上。规则写对,几何前提是错的。

这也就是为什么只看源码看不出问题。源码里该有的模式全有:meta 标签、媒体查询、flex wrap、grid 列——你甚至能想象它截图出来在 Chrome 桌面端缩小到手机比例时看着特别对。可它到了真手机上,布局引擎不认模式,只认几何。那个缺失的 width=device-width 是在渲染管线最外层拧错了一格。

那篇论文把这类问题系统查了一遍,做出来的 WebCompat 在 9 种浏览器和设备组合下跑 2032 个实例。我自己看到那个数字——68% 生成网页至少有一种兼容性问题——第一反应是“模型是不是压根没学 meta viewport”。论文里另一条直接把这个反应按住了:大多数生成网页确实融入了响应式设计模式,只是没有正确实现。不是没写,是写了但没把它安置进整条渲染链路的正确位置。这个区别很关键。没写是一眼能修的低级失误;写了却搭错地方,是真正说明它没有在处理视口几何,只是在输出高概率的字符串组合。

这类字符串组合在训练语料里自然高频:一个高质量移动端页面几乎都会有 <meta name="viewport" content="width=device-width, initial-scale=1"> 和几个媒体查询。模型学会的是“这些 token 经常一起出现”,不是“这个标签决定布局画布,媒体查询基于画布做分支”。所以它能生成整段整段看似专业的响应式代码,却在最关键的那一个前置设置上随机地猜。布局引擎对“像不像”没有容忍度,它只做算术。

那检测怎么办?纯读 CSS 和 DOM 源码基本没戏。几何错误在源码里没有文本形态——刚才那个失败页面里的 @media (max-width: 600px) 和正确页面里的写出来一模一样。必须拿渲染后的视觉结果跟 DOM 计算几何去对。XCompat 干的事就在这条路线上:输入视觉截图和 DOM 树,离线跑,产出 0.903 的 F1。它比现有的兼容性工具和 LLM 基线高不少,根因不复杂——它不是看代码写得对不对,是看画出来的布局有没有对不上几何预期的地方。截图上元素缩成一坨,DOM 里声明的宽度却是 900px,这个不一致就是bug最硬的证据。

我把我们上面那个迷你模型扩一步,也能凑出一个检测骨架:拿到物理屏宽 375,DOM 解析出某块声明宽 900,而渲染截图里的实际物理宽只有 344.4,这个差值直接暴露“设置了 900px 却按 980 画布渲染”的视口错误。真实实现比这多得多——视觉和 DOM 对齐、跨浏览器差异归一化、多类失效的区分——但核心思路是同一套:让布局引擎的几何留下来做证据。

所以说到底,模型生成页面最大的兼容性毛病不在 CSS 属性层的语法错误,而在它把响应式当成了一系列离散 pattern 的粘贴。布局引擎偏偏不吃 pattern,它只看画布多宽、盒子多大、缩放多少。你把 @mediaflex 抄得再全,画布选错了,后面全都朝错误的方向算。这一层想再往下挖,可以动手把我们这个迷你视口模型接进一个只处理宽度和内边距的最小盒模型,然后追踪同一个 DOM 在 980 和 375 两种画布下布局结果的差异——不用多复杂,两百行以内,你就能看见几何从哪一行开始分岔。

陈渊
陈渊

据守底层,挑「会用却说不出为什么」的 CS 机制从第一性原理挖到底。

查看主页 →