THỦ THUẬT HAY

Pretext: Đo lường văn bản không cần DOM – giải pháp né layout reflow cho web hiệu năng cao

Tóm tắt nhanh

  • Pretext là một thư viện JavaScript/TypeScript thuần do Cheng Lou (cựu kỹ sư React core) phát triển, chuyên đo lường và layout multiline text mà không chạm DOM.
  • Hoàn toàn né các API tốn kém như getBoundingClientRect, offsetHeight – vốn trigger layout reflow, một trong những operation đắt đỏ nhất của trình duyệt.
  • Dùng chính font engine của trình duyệt làm ground truth thông qua Canvas 2D measureText, cho kết quả chính xác mà không cần render DOM.
  • Cho phép render text ra DOM, Canvas, SVG, WebGL, và (sắp tới) server-side.
  • Mở khoá nhiều use case mà CSS không làm được tốt: virtualization chính xác, masonry layout, shrink-wrap text, balanced text, ngăn layout shift, và verify label overflow ngay tại development time.
  • Cài bằng một dòng: npm install @chenglou/pretext. API tách thành hai pha rõ ràng: prepare() (một lần) và layout() (hot path).
  • Hỗ trợ CJK, RTL, soft hyphen, letter-spacing, đa ngôn ngữ qua Intl.Segmenter. License mã nguồn mở trên GitHub.

Bất kỳ kỹ sư frontend nào từng làm việc với danh sách dài, chat UI, hay editor đều biết một sự thật khó chịu: đo chiều cao của một đoạn text trên web là cực kỳ đắt. Bạn buộc phải render text vào DOM rồi gọi offsetHeight hoặc getBoundingClientRect. Mỗi lần gọi như vậy, trình duyệt phải dừng pipeline render và tính lại layout của toàn bộ subtree liên quan – thao tác này được gọi là layout reflow và là nguyên nhân gây ra các pha jank kinh điển trong React/Vue app.

Hệ quả: khi bạn implement virtualization (chỉ render row đang hiển thị), bạn buộc phải đoán chiều cao row hoặc dùng nhiều fallback như “ước lượng trước, render rồi cache”. Tương tự với masonry layout, balanced text, hay đơn giản là verify một label trên button có overflow xuống dòng thứ hai hay không – mỗi lần đều phải đi qua DOM.

Pretext lật ngược toàn bộ vấn đề này. Bằng cách dùng chính font engine của trình duyệt qua Canvas 2D measureText (vốn không trigger reflow), thư viện này có thể trả lời câu hỏi “đoạn text này cao bao nhiêu pixel với font X, width Y, line-height Z?” bằng pure arithmetic – không chạm DOM, không pause render pipeline.

Triết lý thiết kế: Hai pha, hai use case

Pretext chia API thành hai pha rõ ràng – đây là điểm thiết kế thanh lịch nhất của thư viện:

  • Pha prepare() chạy một lần cho mỗi cặp (text, font): normalize whitespace, segment text theo Unicode grapheme, apply glue rules, đo width từng segment bằng canvas, rồi trả về một opaque handle.
  • Pha layout() là hot path: pure arithmetic trên cached widths, gọi mỗi khi width thay đổi (ví dụ user resize browser).

Triết lý này khiến Pretext cực kỳ phù hợp cho các kịch bản như responsive virtualization: bạn prepare() toàn bộ data một lần khi load, rồi layout() lại bất cứ khi nào layout pane thay đổi – chi phí gần như zero.

Pretext phục vụ hai use case chính tương ứng:

Use case 1: Đo chiều cao paragraph mà không cần DOM

Đây là 80% nhu cầu thực tế:

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

const prepared = prepare('Hello world example', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// pure arithmetic - không DOM layout & reflow

Kết quả: bạn biết chính xác đoạn text này sẽ cao bao nhiêu nếu render trong container rộng 320px với line-height 20px. Không cần render thử, không cần guess.

Use case 2: Tự layout từng dòng

Khi bạn cần render text ra Canvas, SVG, hay WebGL (như game UI, PDF preview, infinite canvas), Pretext cho bạn API low-level:

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

const prepared = prepareWithSegments('AGI is coming.', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26)
for (let i = 0; i < lines.length; i++) {
  ctx.fillText(lines[i].text, 0, i * 26)
}

Hay scenario phức tạp hơn – flow text around một floated image với width thay đổi theo từng dòng:

let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
  const width = y < image.bottom ? columnWidth - image.width : columnWidth
  const range = layoutNextLineRange(prepared, cursor, width)
  if (range === null) break
  const line = materializeLineRange(prepared, range)
  ctx.fillText(line.text, 0, y)
  cursor = range.end
  y += 26
}

Đây là kiểu layout mà CSS truyền thống không làm được clean – bạn phải hack với shape-outside hoặc float, mà cả hai đều rất giới hạn.

Các use case mở khoá bởi Pretext

Theo tài liệu chính thức, Pretext mở khoá những use case mà trước đây cực kỳ khó hoặc tốn kém:

  • Virtualization chính xác mà không cần guesstimate hay caching layer phức tạp – bạn biết trước chính xác chiều cao mỗi item.
  • Masonry layout JS-driven, flexbox-like custom implementation – những thứ CSS Grid/Flexbox không lo được trọn vẹn.
  • Shrink-wrap text: tìm container width tối thiểu vẫn fit toàn bộ text. Browser web vốn không hỗ trợ điều này natively. Pretext có measureLineStats()walkLineRanges() để binary search width.
  • Balanced text layout: text wrap đều giữa các dòng (giống CSS text-wrap: balance nhưng kiểm soát thủ công).
  • Verify label overflow tại dev time: đặc biệt giá trị trong workflow AI-assisted – bạn có thể verify text labels không overflow mà không cần browser.
  • Anti layout-shift: prevent layout shift khi text mới load và bạn cần re-anchor scroll position.

Hướng dẫn cài đặt Pretext

Bước 1: Install package

npm install @chenglou/pretext

Hoặc dùng pnpm, yarn, bun tương đương. Pretext không có peer dependency nặng nề – thuần JS/TS.

Bước 2: Kiểm tra runtime tương thích

Pretext yêu cầu:

  • Intl.Segmenter (Node 16+, Chrome 87+, Safari 14.1+, Firefox 125+)
  • Canvas 2D text measurement (hầu hết môi trường web hiện đại đều có; server-side cần node-canvas hoặc tương đương)

Nếu bạn dùng Next.js, Vite hay bất kỳ bundler nào hiện đại, không cần config gì thêm.

Bước 3: Sync font config với CSS của bạn

Đây là bước quan trọng nhất và là chỗ dễ sai nhất khi mới dùng Pretext. Tham số font truyền vào prepare() phải match chính xác CSS font của element bạn đang đo:

// CSS:  .my-text { font: 500 16px Inter; letter-spacing: 0.2px; line-height: 24px; }
const prepared = prepare(text, '500 16px Inter', { letterSpacing: 0.2 })
const { height } = layout(prepared, containerWidth, 24)

Lưu ý quan trọng: system-ui không an toàn cho layout() accuracy trên macOS – hãy dùng named font cụ thể (Inter, SF Pro, Roboto…).

Bước 4: Demo project local

Nếu muốn xem demo chính thức:

git clone https://github.com/chenglou/pretext.git
cd pretext
bun install
bun start
# Mở /demos/index trên browser

Trên Windows dùng bun run start:windows. Hoặc xem live tại chenglou.me/pretext.

Ứng dụng thực tế: Một số mẹo nâng cao

Tích hợp với virtualization library

Khi dùng với react-window hay react-virtualized, bạn có thể trả về itemSize chính xác từ Pretext thay vì estimate:

const itemSize = (index) => {
  const prepared = prepareCache.get(items[index].id) ?? 
    prepare(items[index].text, '14px Inter')
  return layout(prepared, listWidth, 20).height + ITEM_PADDING
}

Nhớ cache prepared object – đừng gọi lại prepare() cho cùng text mỗi lần render.

Hyphenation cho manual layout

Pretext không tự động hyphenate. Nhưng bạn có thể chèn soft hyphen (\u00AD) vào text trước khi prepare(). Pretext sẽ xem soft hyphen như optional break point: nếu không cần break thì invisible, nếu break thì hiển thị dấu -.

Với text đa ngôn ngữ hoặc user-generated, ưu tiên locale-aware insertion thay vì pattern hyphenation aggressive.

Rich-text inline với chips, mentions

Pretext kèm helper @chenglou/pretext/rich-inline cho rich-text inline flow với atomic items như mention @maya hoặc chip:

import { prepareRichInline, walkRichInlineLineRanges } from '@chenglou/pretext/rich-inline'

const prepared = prepareRichInline([
  { text: 'Ship ', font: '500 17px Inter' },
  { text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },
  { text: "'s update", font: '500 17px Inter' },
])

break: 'never' đảm bảo @maya không bị wrap giữa chừng, extraWidth: 22 tính cả padding của chip background.

Caveats và giới hạn

Pretext hiện chưa cover toàn bộ CSS text features. Cụ thể:

  • Hỗ trợ white-space: normalpre-wrap; chưa support pre, nowrap đầy đủ.
  • word-break: normalkeep-all; chưa có break-all.
  • font-optical-sizing, font-feature-settings, standalone font-variation-settings chưa được model riêng. Variable font axes chỉ giúp nếu được phản ánh trong canvas font shorthand (qua weight).
  • Tabs default tab-size: 8.
  • Không có automatic hyphenation built-in.
  • Server-side rendering chính thức “sắp tới” theo lời tác giả.

Pretext là một dự án ít ồn ào nhưng kỹ thuật cực kỳ chắc tay – đúng phong cách Cheng Lou. Nếu bạn từng phải vật lộn với virtualization, masonry, hay performance issue liên quan đến text measurement, đây là thư viện đáng add vào toolkit ngay hôm nay. Triết lý “do work once, query cheap” của API prepare()/layout() cũng là một bài học thiết kế đáng học hỏi.

Duy Nghiện
Hãy làm khán giả, đừng làm nhân vật chính :)

You may also like

Nhận thông báo qua email
Nhận thông báo cho
guest

0 Bình luận
Mới nhất
Cũ nhất Nhiều like nhất
Phản hồi nội tuyến
Xem tất cả bình luận