Web 排版三十年没解决的问题,被一个做 AI 的人用 500 行代码解决了

张开发
2026/4/16 22:58:53 15 分钟阅读

分享文章

Web 排版三十年没解决的问题,被一个做 AI 的人用 500 行代码解决了
Web 排版三十年没解决的问题被一个做 AI 的人用 500 行代码解决了文字流动环绕图片、适应不规则形状——印刷品这么做了一百多年。浏览器不行。准确地说浏览器在 30 年里断断续续想做但每次做到一半就卡在同一个地方性能。直到一个从 AI 公司出来的工程师用了一套完全不同的思路500 行代码把这件事做成了。一个被接受了 30 年的常识在说 Pretext 之前先理解为什么这个问题存在了这么久。前端工程师都知道getBoundingClientRect()这个 API。// 当你调用这个的时候constheightelement.getBoundingClientRect().height;这一行代码做了什么它触发了一个叫layout reflow的操作——浏览器要重新计算页面上所有元素的位置和尺寸。为什么是所有元素因为元素的宽高取决于它的父元素父元素取决于它的父元素……一次调用整个 layout 树都要重新跑。你改一个字触发 reflow。你 resize 窗口触发 reflow。你动态插入一个元素触发 reflow。这不是一个 API 的问题。这是浏览器渲染模型的设计问题。只要你依赖浏览器来测量文字你就逃脱不了 reflow。而 reflow 是在主线程上同步执行的——它会阻塞所有其他事情所以复杂布局会卡顿。所以行业内不是不知道这个问题。每个人都知道 reflow 是瓶颈。但没人能绕过去因为绕过它意味着你要自己实现一套文字测量逻辑——这件事太难了回报又不够明显。所以大家接受了这个常识浏览器里做高端排版就是做不了。Cheng Lou 不接受这个常识。Pretext 做了什么Cheng Lou 的解法分两步走第一步prepare() — 用 Canvas 把文字数字化DOM reflow 贵的根本原因是你问浏览器这段文字多宽它要跑一整套 layout 算法。Pretext 的做法是不要问浏览器自己测。import{prepare}fromchenglou/pretext;constpreparedprepare(Hello 世界 ,16px Inter);这行代码做了以下事情把文字拆成最小单元中文按字英文按词emoji 单独拆出来做成一个 segment 列表用 Canvas 测每个 segment 的宽度调用canvasCtx.measureText()这是 Canvas API不是 DOM API不会触发 reflow。Canvas 底层用的是浏览器内置的字体渲染引擎所以测量结果是准的把结果缓存起来每个 segment 的宽度存在 prepared 对象里这一步大约 19ms可以做 500 条文本。每条文本跑一次就够了。第二步layout() — 纯算术算出行高有了每个字的宽度接下来的事情就是小学数学import{layout}fromchenglou/pretext;// 给定最大宽度 500px行高 20pxconst{height,lineCount}layout(prepared,500,20);layout()的逻辑是这样的当前行已用宽度 0 遍历每个字形 如果 当前字形宽度 当前行已用宽度 最大宽度 放在当前行 否则 换行当前字形放下一行当前行已用宽度 字形宽度没有任何 DOM 操作。没有任何 reflow。就是数组遍历和加法。这一步约 0.09ms对 500 条文本可以反复调用。为什么这样快对比一下传统方式以 DOM 为基础问浏览器 → 触发 layout reflow → 主线程卡住 16ms → 返回结果Pretext 方式第一次Canvas 测量 19ms → 缓存宽度数据 之后每次数组遍历 加法 0.09ms → 返回结果差距在哪里第一次测量的 19ms 是必要的但只需跑一次之后的 layout 是 0.09ms比一次 DOM reflow 快几百倍而且 0.09ms 可以放在动画循环里跑不会卡 UI这就是为什么 resize 窗口的时候 Pretext 可以实时重新计算文字高度而传统方案只能靠 debounce 减少计算次数。它解锁了什么有了这个能力之前绕不过去的几个场景变得可以做了文字环绕浮动图片用 Pretext算出文字在限定宽度内的高度然后用这个高度去设置图片容器的浮动区域。图片动文字跟着重算。以前实现不了是因为文字环绕需要知道文字在哪一行想知道在哪一行需要知道图片在哪图片位置变了又要重新算文字——这个循环每次都要触发 reflow。用 Pretext每帧重算一次 layout() 代价极低可以直接放在动画循环里。虚拟列表不用猜高度了无限滚动最大的痛每条内容高度不一样传统方案靠估算 实测更新来绕。// 精确知道每个 item 需要多高constitemHeightlayout(preparedItems[i],itemWidth,lineHeight).height;// virtual list 直接算好偏移量不需要 guess update防止 layout shift页面加载时用layout()预先算出所有文字容器的高度先把高度占好文字进来不会把下面的内容挤下去。用户体验就是没有跳的感觉。Canvas 文字排版Canvas 的fillText()不自动换行。以前要自己实现断行算法效果总是差一口气。const{lines}layoutWithLines(prepared,canvasWidth,lineHeight);for(constlineoflines){ctx.fillText(line.text,0,y);ylineHeight;}每一行的实际宽度也返回了可以用ctx.fillText(line.text, offsetX, y)做居中或右对齐——第一次在 Canvas 上做出了接近印刷质量的排版。为什么是 Cheng Lou 做到的说到这有必要聊聊这个人。React Motion 是他写的。业界最早把动画这件事用物理引擎的方式重新表述不是动画到第几帧而是弹簧刚度多少、阻尼多少。ReasonML 他是合创者。OCaml 方言用来让函数式编程在工业界变得更可实践。Facebook Messenger 的前端重构他是核心贡献者。Messenger 那种量级的实时聊天界面对性能的要求是极其苛刻的。然后他去了 Midjourney。现在他做的 Pretext 是一个前端工具——他做 AI 的同事觉得这个方向不可思议一个做 AI 模型训练的工程师跑回去解决了一个 30 年的浏览器问题。但细想这个逻辑它其实很自然。AI 工程师在重新看老问题Cheng Lou 在 Midjourney 训练模型。模型训练有一个核心循环给定 ground truth迭代优化测量误差repeat。他在 Pretext 的文档里写了一句很有意思的话迭代方法论是AI-friendly的。意思是他用模型训练的方式迭代这个库——测数据、调参数、看 ground truth 的偏差、再测、再调。这个思路和做 AI 模型是完全一样的。CSS Working Group 的人是浏览器工程师他们想的是如何在现有架构里把这个 case 修掉。Cheng Lou 是 ML 工程师他想的是这个问题本质上在测什么我能不能从第一性原理重新设计测量流程。这是两种完全不同的思维方式。后者看一个 30 年没解决的问题第一反应是它真的是个难问题还是我们一直在用错误的方法解一个信号这篇文章不是要讲一个大神跨界成功的励志故事。它说的是AI 行业培养出来的这批人他们的第一性原理思维和迭代方法论正在流向那些被整个行业接受为已知局限的地方。文本测量是一个。前端渲染性能是一个。这些地方不性感不吸引融资但它们是真实应用的基础设施。当 AI 工程师开始认真看它们用训练模型的思路重新解这些题产出的是 Pretext 这种东西——500 行代码替代了过去 30 年的妥协方案。对行业来说这是一个值得关注的信号。参考资料1. Pretext GitHubhttps://github.com/chenglou/pretext2. Pretext Live Demoshttps://chenglou.me/pretext3. “Web Typography Just Caught Up To The Page, And A Midjourney Engineer Built The Bridge”https://www.b2bnn.com/2026/03/web-typography-just-caught-up-to-the-page-and-a-midjourney-engineer-built-the-bridge/

更多文章