Web Dev #Web Development #Frontend #JavaScript

Pretext 高效多行文字 Canvas 排版庫優化與防 DOM 重繪技術

想精準計算段落高度、做出更自由的文字排版,卻不想一直碰 getBoundingClientRect?Pretext 提供了一條很聰明的路。

為什麼選擇 Pretext?

在前端處理文字排版時,最麻煩的事情之一就是:你明明只是想知道一段文字會佔多高,卻常常得把它真的丟進 DOM 裡量一次。

getBoundingClientRect()offsetHeight 這類做法雖然直覺,但很容易觸發 layout reflow。當畫面裡有大量動態文字、卡片瀑布流、虛擬列表,或是你想做更進階的自訂排版時,這個成本其實不小。

Pretext 是 Cheng Lou 做的純 JavaScript / TypeScript 文字量測與排版函式庫。它的核心思路很漂亮:不依賴 DOM 量測,而是自己做文字分析與快取,再用瀏覽器的字型引擎作為 ground truth。

它支援多語系、支援 white-spaceword-break 的常見情境,還可以把排好行的結果拿去渲染到 DOM、Canvas、SVG,甚至未來做 server-side。


實作展示 (Official Demo)

這次我其實有先自己試做一下,但目前還沒做出我自己滿意的成品,所以文章先放官方大神做的 demo,真的非常猛。

下面這段影片是我放進部落格的展示:

這個 demo 對應的是 Somnai Dreams 做的 The Editorial Engine

點這裡看官方 Demo

如果你想看更多範例,也可以直接去作者提供的展示頁:


如何在你的專案中使用?

1. 安裝套件

你可以直接安裝 npm 套件:

bash
npm install @chenglou/pretext

2. 核心代碼實作

如果你的需求只是:在不碰 DOM 的情況下,預先算出一段文字在某個寬度下會佔幾行、多高,那最基本的用法其實很簡單:

ts
import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)

console.log(height, lineCount)

prepare() 會先做一次性的預處理,包括文字分段、空白正規化、量測 segment 寬度與快取;layout() 則是後續的快速熱路徑,單純根據寬度與行高做算術計算。

這代表當你的容器寬度改變時,不需要一直重新分析全文字,通常只要重跑 layout() 就好


進一步玩法:自己掌控每一行

如果你不只想知道高度,而是想要自己控制每一行的內容,例如:

  • 讓文字繞圖
  • 做 Canvas / SVG 排版
  • 自己決定每一行的寬度
  • 做 multiline shrink-wrap

那就可以改用 prepareWithSegments()layoutWithLines()

ts
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments(
  'AGI 春天到了. بدأت الرحلة 🚀',
  '18px "Helvetica Neue"'
)

const { lines } = layoutWithLines(prepared, 320, 26)

for (let i = 0; i < lines.length; i++) {
  console.log(lines[i].text, lines[i].width)
}

這種 API 很適合拿去做比較「設計導向」的文字編排,而不是只能接受瀏覽器幫你決定一切。


我覺得 Pretext 厲害的地方

1. 先預處理,之後只做便宜計算

這個設計很適合 responsive UI。文字內容沒變時,你可以把 prepare() 的結果保留起來,視窗縮放只更新 layout()

2. 對多語系文字更友善

官方範例直接拿中英阿拉伯文和 emoji 混排,這點很有說服力。很多看似簡單的文字排版工具,一碰到多語混排就開始露出破綻。

3. 很適合做 AI 時代的快速 UI 驗證

README 裡有一個觀點我很認同:現在很多 UI 是快速迭代、甚至直接讓 AI 幫你生畫面,這時候如果能在不開瀏覽器、不碰 DOM的前提下預先驗證文字會不會爆行,真的很實用。

4. 不只量高度,還能拿來做自訂排版引擎

layoutNextLineRange()materializeLineRange() 這些 API,其實已經不是單純量測工具而已,幾乎是在提供你一套低階但很實用的文字流排版能力。


幾個值得注意的限制

Pretext 很強,但它也沒有假裝自己是完整的瀏覽器排版引擎。

  • 目前主要鎖定 white-space: normal / pre-wrap
  • 支援 word-break: normalkeep-all
  • 非常窄的寬度下,仍可能在 grapheme 邊界做 break-word 式換行
  • 依賴 Intl.Segmenter 與 Canvas 2D text measurement
  • 在 macOS 上,system-uilayout() 精準度不安全,官方建議用具名字型

所以它最適合的場景不是「完整取代瀏覽器排版」,而是:你想提前知道排版結果,或者你就是要自己接管文字流布局。


個人心得

我很喜歡 Pretext 這種工具的出發點:不是把整個世界重做一遍,而是專注解掉前端裡一個很痛、但大家常常默默忍受的問題。

這次我原本也想自己做一個更完整、比較有作品感的示範,不過老實說目前還沒有做到我滿意,所以先把官方大神的 demo 放上來。Somnai Dreams 那個 The Editorial Engine 真的把 Pretext 的潛力拉得很高,已經不是「技術展示」而已,而是很接近一種新的文字互動介面。

如果你現在剛好在做以下東西,我會很建議研究一下 Pretext:

  • 需要預估文字高度的虛擬列表
  • 瀑布流或卡片式排版
  • Canvas / SVG 文字排版
  • 圖文混排或繞圖排版
  • 想避免 layout shift 的內容載入介面

小建議:如果你只是 resize 後重新計算高度,記得不要每次都重跑 prepare(),不然就浪費掉 Pretext 最核心的快取優勢了。


相關連結:


本文展示影片採用 Somnai Dreams 製作的 Pretext 官方社群 demo;文中 API 說明整理自 Pretext README 與公開範例。