你大概率曾经注意到，在手机上用原生 app 刷信息流的时候，滚动通常很顺滑。但在浏览器里打开一个内容密集的网页，快速滑动时经常会看到内容跳动、布局闪烁、空白区域先出现再被填上内容。这不是网速的问题。很大一部分原因是，浏览器在计算"每段内容排完版占多大空间"这件事上，比原生平台慢了一个数量级。

[Pretext](https://yage.ai/share/pretext-short-term-overrated-long-term-underrated-20260330.html) 最近在技术社区很火，做的就是这件事：预测一段文字放进容器后会占多少空间。一位叫马工的读者看完后问了一个很好的问题：这不应该是最基本的功能吗？为什么到 2026 年了还需要一个第三方库来解决？

我顺着这个问题往下挖，发现它触及的东西远比前端技术选型深。Facebook 在 2012 年因为没理解它背后的 trade-off，付出了重写整个移动端的代价。这不只关乎前端工程师。它是一个关乎系统设计哲学的判断：你在做抽象的时候，该把什么藏起来，该把什么留在外面。

## 在其他平台上，这真的就是一行代码

先验证马工的直觉。在 iOS 上，你想知道一段文字在给定宽度下排完版占多高，调用 `sizeThatFits` 就行。一行代码，立刻返回，甚至不需要把这段文字放进任何界面。Android 上用 `StaticLayout`，Qt 用 `QFontMetrics`，Flutter 用 `TextPainter`，都是同一个模式：排版引擎是一个独立的计算模块，你给它输入，它给你输出，不触发任何全局操作。

这就是为什么原生 app 滚动长列表的时候通常很流畅：系统可以提前算好每条内容的高度，精确地知道哪些内容即将进入屏幕、该为它们预留多少空间。

Web 上做不到这件事。在浏览器里想知道一段内容排完版多高，你得把它真的放进页面，然后触发一次 Reflow，也就是浏览器重新计算页面中所有受影响元素的位置和大小。这个计算是同步阻塞的，而且范围不是只算你问的那个元素，而是可能波及整棵布局树。一个内容密集的信息流页面，窗口大小一变，每条内容都要重算一遍，每次重算都牵动整个页面。这就是为什么 Web 版的信息流、聊天界面、电商列表在滚动和窗口缩放时，体验通常比原生 app 更差。

马工说得对：这确实应该是最基本的功能。而且在 iOS、Android、Qt、Flutter 上，它真的就是最基本的功能。Web 是主流 UI 平台中唯一一个做不到这件事的。

但 Web 工程师不可能三十年都没想到。这里一定有一个 trade-off。而且理解这个 trade-off 很重要。

## CSS 的选择不是因为蠢

构建界面有两种方式。

一种是你告诉系统每个东西放哪里。早期 iOS 开发就是这样，你手动计算每个元素的坐标和尺寸。系统严格执行，你对每一步都有精确理解。在这种方式下，"排完版了多高"是一个自然的中间数据，因为你本来就要用它来决定下一个元素放在哪。

另一种是你描述你想要什么效果，让系统自己算。CSS 就是这样。你写 `display: flex; flex-wrap: wrap; gap: 16px`，不管屏幕是 320px 还是 1920px，浏览器自己决定每行放几个元素、怎么分配空间。你不控制过程，你描述意图。

后者的天花板更高。LaTeX 是更极端的例子。它的断行算法把整个段落当作一个优化问题来求解，考虑所有可能的断行方案，选一个让全段行间松紧度最均匀的。它可能会让前面几行排得稍松，来避免后面某一行出现难看的大空隙。你用逐行手动排版写不出这种效果，因为排第三行的时候你不知道第七行会怎样。只有一个能看到全局的系统才能做这种优化。

CSS 的响应式布局也是同一个逻辑。有一位做过 WPF 和 XAML 的开发者在 Hacker News 上说，用 CSS Flexbox/Grid 做自适应布局比很多原生桌面框架的开发效率更高，因为你在描述意图而不是在编写实现。

但代价是什么？代价是你没法问系统"你算出来的结果是什么"。在 CSS 的世界里，一个元素的最终大小取决于它周围的所有元素。浮动、定位、行内格式化上下文、margin collapse，每一条规则都在强化同一个事实：局部结果是全局求解出来的，不存在脱离上下文的独立答案。1994 年 CSS 的第一份提案就确立了这个架构：信息单向流动，开发者声明规则，浏览器执行排版，但浏览器不反馈中间结果。

在文档排版的场景下，这完全合理。你不需要知道段落精确多高，你只要声明样式，浏览器负责呈现。LaTeX 用户也一样。你不控制图片出现在哪一页，你告诉 TeX 你的偏好，它自己决定最优位置。

不理解这个 trade-off 的代价可能高达上亿美元。2012 年，Facebook 用 HTML5 构建了整个移动端。iOS 和 Android 的 app 本质上是对 WebView 的封装。选择 HTML5 确实直观：write once, run everywhere，服务端推送更新不需要用户下载新版本。但最终大家发现，有个致命问题是性能扛不住。Facebook 工程师[事后复盘](https://www.infoq.com/news/2012/09/Facebook-HTML5-Native/)的时候列举了一系列问题：滚动帧率不稳定、UI 线程卡顿、设备资源耗尽导致崩溃。DOM 的全局 Reflow 是核心瓶颈之一：每次内容更新都可能触发整棵布局树的重新计算，News Feed 那种长列表加大量图片的场景直接把它推到了极限。

最终 Facebook 花了9个月从头重写原生 iOS 应用。启动时间从约10秒降到约4秒，News Feed 加载速度提升一倍。Zuckerberg 在 TechCrunch Disrupt 上说：

> The biggest mistake that we made as a company was betting too much on HTML5 as opposed to native.

后续的发展也很值得讨论。HTML5 失败直接催生了 React（2013），React 的声明式思路被验证后催生了 React Native（2015），React Native 的布局引擎 Yoga 在设计上明确优化了排版查询。[Facebook 工程博客](https://engineering.fb.com/2016/12/07/android/yoga-a-cross-platform-layout-engine/)写道，Yoga 确保文本视图"只被测量尽可能少的次数，理想情况下只测量一次"，并且把布局计算放到了独立线程上，彻底绕开了 DOM Reflow 中 JavaScript 和布局互相阻塞的问题。

这每一步都在逃离 CSS 布局架构的限制。因此，理解 CSS 为什么做了这个 trade-off，以及这个 trade-off 在什么场景下会崩溃，是一个价值数亿美元的判断。

## 但这个 trade-off 不是必须的

到这里，故事听起来像一个非此即彼的选择：要么选"系统替你决定"拿到更好的排版效果，代价是看不到中间结果；要么选"你自己控制一切"拿到精确可查询性，代价是失去全局优化。

但事实上，这个 trade-off 是可以打破的。

SwiftUI（Apple 2019 年推出）和 Jetpack Compose（Google 2021 年推出）都是声明式 UI 框架。你用 SwiftUI 写响应式布局的方式和用 CSS 写 Flexbox 在概念上非常接近：描述意图，系统自行决定布局。但它们都没有 CSS 的问题。

内里的原因在于架构分层。原生平台的设计是：底层有一个完全独立的排版引擎（iOS 的 Core Text，Android 的 StaticLayout），上层是声明式框架。声明式框架调用底层引擎来完成布局，但应用代码也可以随时穿透声明式抽象，直接调用底层引擎查询排版结果。Jetpack Compose 甚至提供了官方的 `TextMeasurer` API，可以在不触发任何实际渲染的情况下拿到完整的排版信息，包括宽高、行数、每个字符的位置。

这证明了一个重要的事情：声明式和可观测并不矛盾。你可以让系统替你做全局优化，同时保留一条独立的通道让你查询中间结果。关键是排版引擎要被设计成一个独立模块，而不是焊死在全局布局管线里。

CSS 的问题不是它选择了声明式。问题是它在做抽象的时候，把排版引擎也封进了全局布局流程，没有留一个独立的查询接口。（叠甲：`canvas.measureText()` 只能处理单行文本，不支持换行。处理换行的那套逻辑被锁在了布局引擎内部，从未作为独立接口暴露。）

1994 年设计 CSS 的时候，Web 是一个学术文档交换系统，没人预见到它会成为应用平台。在文档场景下，你确实不需要独立查询排版结果，所以没人觉得需要把排版引擎设计成可以单独调用的模块。这个决策在当时完全合理。但三十年后，它成了一笔巨大的技术债。

W3C 也对这个问题心知肚明。CSS Houdini 的 Font Metrics API 就是为了解决它，但截至 2026 年仍停留在提案阶段，没有浏览器实现。Pretext 用4000行用户态代码补了这个缺口。它在 Web 上重建了一个 iOS 和 Android 早就作为基础设施提供的能力。它的存在本身就是 CSS 缺失了一层抽象的证据。

## 这个教训比前端大得多

回头看，CSS 和原生平台都选择了声明式布局，但原生平台保留了排版引擎作为独立的可查询层，CSS 没有。一个在 1990 年代做出的、当时看起来无关紧要的分层决策，在三十年后导致了完全不同的开发体验，完全不同的性能特征，和一家公司数亿美元的战略代价。

这个模式在前端之外同样存在。

传统 API 设计遵循的哲学和 CSS 一样：隐藏复杂性，保护用户不需要看到中间状态。捕获底层错误后抛出一个抽象的高层异常，把实现细节封装在干净的接口后面。这在用户是人类的时候没问题。人类的认知带宽有限，好的抽象帮他们聚焦。

但当系统的用户变成需要做决策的代码或 AI 的时候，这种保护性抽象就成了障碍。AI 的有效性依赖于[尝试-反馈-修正的循环](https://yage.ai/result-certainty.html)。一个模糊的"操作失败，请稍后再试"会直接中断这个循环，和 CSS 不告诉你排版结果是同一个问题。AI 需要的是[精细原始的反馈、细粒度的控制接口、足够详尽的中间状态](https://yage.ai/ai-software-engineering.html)。前端排版三十年的挣扎和 AI 工程今天遇到的问题，根源是同一个设计哲学在不同时代的碰壁。

解法也是类似的。Pretext 在 CSS 的声明式渲染旁边建了一条独立的可查询通道，不替代浏览器的排版，只是让你能观测到排版结果。Agentic loop 对 AI 工作流做了同样的事：不规定 AI 每一步怎么做，但让它能观测到自己行动的结果，然后自主决定下一步。两者都没有拆掉抽象，而是在抽象上开了观测窗口。

马工说"前端很多问题都是因为缺乏合适的架构造成的"。这个方向是对的，但可以更具体精确：问题不在于 CSS 选择了声明式，声明式的天花板确实更高。问题在于 CSS 在做抽象的时候，把不该藏的东西也藏了。好的抽象让你选择在哪一层工作，坏的抽象把所有层粘在一起让你没得选。

下次你设计一个系统、做一层抽象的时候，值得问自己一个问题：你有没有把使用者未来需要观测的中间状态封进了黑盒？这个决策今天可能看起来干净优雅。但 CSS 用三十年证明了，这可能是一笔到期时间很长、利息很高的技术债。

<script async data-uid="49b08a3e73" src="https://yage-ai.kit.com/49b08a3e73/index.js"></script>
