Skip to main content

當文字測量不再需要 DOM,前端的想像力就被解放了

3 min read
pretext-userland-text-measurement-without-dom

最近無聊滑著 X 上看到 Cheng Lou(對,就是那個做過 React、ReasonML、ReScript,後來去了 Midjourney 的 Cheng Lou)發了一篇超級炸裂的 thread,三天就破 2100 萬瀏覽、6 萬多 likes、6 萬 bookmarks。

他說他「爬過了地獄的深淵」帶來了一個東西:純 TypeScript 的 userland 文字測量演算法,可以在完全不碰 CSS、不觸發 DOM measurement 和 reflow 的情況下,排版整個網頁。

我看完整個 thread 加上所有 demo 之後,覺得這東西的意義可能比表面上看起來的還要大。


先搞懂問題:為什麼文字測量這麼痛?

如果你寫過任何需要「知道文字有多高」的前端功能,你一定碰過這個困境。

想做一個 virtualized list,每個 item 裡面有不同長度的文字?你得先把元素 render 到 DOM 裡,用 getBoundingClientRect() 量出高度,然後才能計算哪些 item 該出現在 viewport 裡。

這個流程有幾個致命的問題:

  1. DOM read/write interleaving:你讀一個元素的尺寸,瀏覽器就得先把 pending 的 layout 算完(reflow),然後你再寫入新的 style,再讀下一個——這是 web 上最貴的操作之一
  2. Batching 的噩夢:為了效能你得把所有的讀取操作 batch 在一起,但這完全破壞了 component 的封裝性。你的 component 不能自己量自己的高度,得交給某個外部的 batch manager
  3. 非同步的痛苦:量測結果是非同步的,你的 layout 邏輯就變成一堆 callback 和 state sync

Cheng Lou 在 thread 裡說,這個 paradigm 比傳統的 getBoundingClientRect() 做法快了大約 500 倍(他自己也說這個比較不太公平,但量級差距是真的)。最大的效能提升不是來自微優化,而是來自架構層面的轉變——當你根本不需要 DOM 來量文字,整個 layout 的 programming model 就不一樣了。


Pretext 到底做了什麼?

簡單來說,Pretext 是一個用純 TypeScript 寫的文字測量引擎。你給它一段文字、字體資訊和容器寬度,它就能告訴你每一行會在哪裡斷行、每一行有多高、整個文字區塊佔多少空間——完全不需要碰 DOM

引擎本身很小(只有幾 KB),而且:

  • 處理了各種瀏覽器的 quirks
  • 支援多語言,包括韓文混合 RTL 阿拉伯文
  • 支援各平台的 emoji
  • 支援 variable fonts

最酷的是他怎麼做到的——他讓 Claude Code 和 Codex 去對照瀏覽器的 ground truth,在每一個有意義的容器寬度上反覆測量和迭代,跑了好幾個禮拜。用 AI 來磨出一個精確的文字測量模型,這個方法本身就很有啟發性。


那些讓人嘴巴張開的 Demo

Cheng Lou 在 thread 裡放了五個 demo,每一個都在展示:「當你解決了文字測量的問題,什麼東西變得 trivial 了」。

1. 十萬個文字區塊的 Virtualization

不同高度的文字區塊,不需要 DOM 測量,visibility check 簡化成一次線性的高度遍歷,滾動和 resize 都跑在 120fps。

以前要做這個,你得用 react-virtualizedreact-window,然後還得處理動態高度的問題(通常是先 render 再量,或是給一個 estimated height)。現在?直接算就好了。

2. 自動收縮的 Chat Bubble

聊天氣泡的寬度要剛好包住文字,不多不少。聽起來簡單?用 CSS 做過的人都知道那些 max-widthwidth: fit-contentword-break 的組合有多脆弱。

3. 響應式的多欄雜誌排版

想像一個像雜誌一樣的多欄排版,但是是動態的、響應式的。文字會自動 reflow 到不同的欄位裡,根據容器寬度調整。

4. Variable Font 的 ASCII Art

因為文字測量變得精確了,你甚至可以用 variable font width 來做 ASCII art。這個 demo 純粹是在 flex(炫技的那個 flex,不是 CSS 的)。

5. 自動增長的 Textarea、手風琴、Canvas 多行文字

這些都是前端老生常談的痛點。自動增長的 textarea 以前要用 hidden div 來量高度,現在不用了。


自己動手玩玩看

看完 demo 覺得很酷,但總覺得「這真的有這麼神?」——所以我直接裝了 @chenglou/pretext,做了兩個小東西來驗證。

準確度測試

先做最基本的:Pretext 的 layout() 算出來的高度,跟實際把文字丟進 DOM 用 getBoundingClientRect() 量出來的高度,到底差多少?

我測了六種情境——短英文、長英文、純中文、中英混合、Emoji、超長單字 overflow——結果是:

測試PretextDOM誤差
English (short)24.0px24.0px0.00px
English (long)120.0px120.0px0.00px
純中文72.0px72.0px0.00px
中英混合96.0px96.0px0.00px
Emoji72.0px72.0px0.00px
Long word overflow72.0px72.0px0.00px

6/6 全部 0.00px 誤差。 不是「差不多」,是完全一樣。說實話我本來預期會有個 1-2px 的落差,結果完全沒有,有點被嚇到 (゚д゚)

效能測試

接著測速度。生成 1000 段隨機的中英混合文字,在 4 種不同寬度(200/320/480/600px)下各測量一次,等於 4,000 次測量:

方式耗時
Pretext prepare() × 1000(一次性)77.4ms
Pretext layout() × 4,0002.8ms
DOM getBoundingClientRect × 4,000208.5ms

關鍵在於 prepare() 只需要跑一次。之後不管你要在多少種寬度下 re-layout,layout() 都是純算術——4,000 次只要 2.8ms。這在 virtualization 和 resize 的場景下,差距會被無限放大。

Playground:自己玩玩看

光看數字可能沒感覺,所以我做了一個互動的 Playground,讓你親手體驗「不靠 DOM 排版文字」是什麼感覺:

👉 Pretext Playground

打開之後你可以這樣玩:

  1. 拖動上方的寬度 slider——看文字在 Canvas 上即時 reflow,注意右邊的 prepare + layout 時間,通常不到 1ms
  2. 試試不同的 Preset——點 ChineseEnglishMixedEmojiCode-like,看 Pretext 怎麼處理不同語言和格式
  3. 換字體和字級——右邊可以選不同的 font family、調整 font size 和 line height,測量結果即時更新
  4. 觀察 Shrink Width——Canvas 上的藍色虛線框就是「最緊的容器寬度」,這個功能在 CSS 裡面一直不存在(多行的 fit-content),但 Pretext 用 walkLineRanges() 一行 code 就算出來了
  5. 看 Line Breakdown——右下角列出每一行的文字和精確寬度,你可以看到 Pretext 是怎麼決定在哪裡斷行的

整個 Canvas 上的文字渲染完全不經過 DOM layout——你看到的每一行都是 Pretext 算好位置後直接用 ctx.fillText() 畫上去的。

如果你想看準確度和效能的完整測試結果,也可以看 驗證頁面


為什麼我覺得這件事很重要

Cheng Lou 在 thread 裡說了一句讓我印象很深的話:有了這個,我們不再需要在「GL landing page 的炫目」和「blog 文章的實用性」之間做選擇。

這句話的意思是:以前如果你想做一個超級酷炫的、像 Awwwards 那種有各種動態效果的網頁,你基本上得放棄正常的文字排版——因為那些效果通常需要在 Canvas 或 WebGL 裡面 render,而文字在那個世界裡是二等公民,你沒辦法自動斷行、沒辦法做 responsive layout。

反過來,如果你要做一個正常的、有大量文字的網頁,你就得老老實實用 DOM 和 CSS,犧牲掉那些炫目的視覺效果。

Pretext 打破了這個二分法。當文字測量可以在 JS 層面完成,你就可以在 Canvas、WebGL、或任何你想要的 rendering target 上面排版文字,同時保有 DOM 排版的精確度。


對 AI 時代的意義

Cheng Lou 特別提到「especially in the age of AI」,我覺得這不是隨便說說的。

AI 生成的內容是動態的、長度不可預測的。如果你在做一個 AI chat 介面,每個回覆的長度都不一樣,你需要精確知道每個 message bubble 的高度才能做好 virtualization 和動畫。如果你在做一個 AI 驅動的文件編輯器,你需要即時知道文字排版的結果才能做出流暢的 UI。

當文字測量變成純計算(不需要 DOM),這些場景的實現就從「很難但勉強能做」變成了「自然而然」。


一些個人想法

看完這個 thread 之後,我有幾個感想:

1. 最好的效能優化是架構層面的

Cheng Lou 說的那句「The best perf gains come from architectural shifts」,真的是金句。不是說微優化不重要,但如果你能從根本上改變問題的結構,效能提升就是量級上的差距。從 500 次 DOM reflow 到 0 次,這不是什麼 memoization 或 lazy loading 能比的。

2. AI 作為工程工具的新範式

他用 Claude Code 和 Codex 來「學習」瀏覽器的文字測量行為,這個方法很聰明。不是讓 AI 直接寫出完美的演算法,而是讓 AI 做大量的測量、比較、迭代——基本上就是把 AI 當成一個不會累的 QA engineer,跑了幾個禮拜的 regression test。

3. 開源精神依然存在

專案在 GitHub 上開源(@chenglou/pretext),npm install 就能用。在這個什麼都想 SaaS 化的年代,這種「我做了一個很酷的東西,拿去用吧」的精神真的很棒 ヽ(✿゚▽゚)ノ


結語

Cheng Lou 一直是那種「不鳴則已,一鳴驚人」的工程師。從 React 的早期到 ReasonML,每次他出手都是在挑戰一些被認為「就是這樣」的基本假設。

這次也一樣。「文字測量必須透過 DOM」——這個假設存在了多久?大概從 web 誕生以來就是這樣。現在有人說:不一定。而且他不只是說說而已,他做出來了,還附了一堆讓人目瞪口呆的 demo。

如果你是前端開發者,我強烈建議你去看看那些 demo(chenglou.me/pretext/),然後想想:如果文字測量不再是瓶頸,你會想做什麼?

這可能是我今年看到最讓人興奮的前端創新了 (っ°Д°;)っ