TL;DR (Tóm tắt)
Pretext.js là một thư viện TypeScript không phụ thuộc, giúp đo lường và định vị văn bản đa dòng thông qua các phép toán thuần túy thay vì thao tác DOM. Thư viện này loại bỏ reflow đồng bộ bắt buộc, đo văn bản nhanh hơn ~500 lần so với getBoundingClientRect() và hỗ trợ mọi hệ thống chữ viết chính. Nếu bạn xây dựng trình cuộn ảo, giao diện trò chuyện hoặc bảng dữ liệu, Pretext.js là giải pháp thiết thực cho vấn đề mà trình duyệt bỏ ngỏ suốt 30 năm.
Giới thiệu
Khi bạn gọi getBoundingClientRect() hoặc đọc offsetHeight trong JavaScript, trình duyệt sẽ dừng mọi thứ để tính lại bố cục và hiển thị, gây reflow đồng bộ — thao tác tốn kém nhất trong trình duyệt.
Nhân điều này lên với 1.000 bong bóng chat hoặc 10.000 hàng trong bảng dữ liệu, bạn sẽ gặp tình trạng lag, drop frame, và trải nghiệm người dùng kém mượt mà.
💡 Các nhóm Apidog xây dựng UI dựa trên API rất hiểu nỗi đau này; truyền dữ liệu vào UI động mà vẫn giữ mượt là một cuộc chiến không hồi kết nếu bạn phụ thuộc vào đo lường DOM.
Cheng Lou (cha đẻ react-motion, core React/ReasonML tại Meta) phát triển Pretext.js để xử lý vấn đề này. Chỉ trong vài ngày sau phát hành (tháng 3/2026), Pretext.js đã đạt hơn 14.000 sao trên GitHub và thu hút chủ đề lớn trên Hacker News.
Bài viết này sẽ hướng dẫn bạn cách Pretext.js hoạt động, khi nào nên sử dụng, cách tích hợp và các lưu ý thực chiến.
Pretext.js là gì?
Pretext.js là thư viện bố cục văn bản thuần JS/TS, đo và định vị văn bản đa dòng bằng toán học, không dùng getBoundingClientRect(), không offsetHeight, không reflow, không lãng phí hiệu năng.
Thay vì hỏi trình duyệt "văn bản này cao bao nhiêu?", Pretext.js tính toán câu trả lời dựa trên chỉ số font từ Canvas API.
API sử dụng:
import { prepare, layout } from '@chenglou/pretext';
// Bước 1: Chuẩn bị văn bản (có thể cache)
const handle = prepare('Hello, pretext.js', '16px "Inter"');
// Bước 2: Bố cục ở mọi chiều rộng (micro giây)
const { height, lineCount } = layout(handle, 400, 24);
Chỉ với 2 hàm:
-
prepare()đo và cache các đoạn văn bản (gọi CanvasmeasureText()). -
layout()thực hiện toán học thuần túy, không chạm DOM.
Tại sao quan trọng với ứng dụng nặng API
Nếu bạn xây dựng ứng dụng tiêu thụ API streaming (AI assistant, dashboard realtime, editor cộng tác), bạn cần biết chiều cao văn bản trước khi render. Nếu không, virtual scroller sẽ lag, giao diện nhảy, trải nghiệm tệ.
Pretext.js cung cấp chiều cao đó trong micro giây, thay vì mili giây — sự khác biệt này tích lũy rất nhanh.
Vấn đề Pretext.js giải quyết
Giải thích về Reflow đồng bộ
Ví dụ:
const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
const height = el.getBoundingClientRect().height; // REFLOW!
// dùng height để định vị...
});
Mỗi lần gọi getBoundingClientRect():
- Tạm dừng JavaScript
- Flush tất cả thay đổi style
- Tính lại layout toàn trang
- Trả lại giá trị
Với 1.000 phần tử, bạn có 1.000 lần layout lại, ~94ms, mất ~6 frame ở 60fps.
Vấn đề cuộn ảo
Các thư viện cuộn ảo như react-window, tanstack-virtual cần biết chiều cao từng item. Nếu item có chiều cao thay đổi dựa vào text, bạn phải render DOM để đo, hoặc ước lượng rồi chỉnh lại sau — đều gây ra lag và nhảy UI.
Pretext.js giúp bạn tính đúng chiều cao văn bản trước khi có DOM node, loại bỏ giải pháp tạm bợ.
Số liệu benchmark
| Phương pháp | 1.000 khối văn bản | 500 khối văn bản |
|---|---|---|
DOM (getBoundingClientRect) |
~94ms (mất 6 frame) | ~47ms |
Pretext.js (layout()) |
~2ms | ~0.09ms |
| Tốc độ nhanh hơn | ~47x | ~500x |
Tối ưu rõ rệt khi xử lý lô nhỏ vì chi phí đo DOM không đổi, còn Pretext.js tăng tuyến tính.
Cách Pretext.js hoạt động
Ba giai đoạn chính:
1. Phân đoạn văn bản
prepare() chuẩn hóa văn bản, xử lý khoảng trắng, áp dụng quy tắc ngắt dòng Unicode (UAX #14), phân đoạn thành các đơn vị có thể ngắt.
Hỗ trợ đầy đủ:
- CJK (Trung, Nhật, Hàn)
- RTL (Ả Rập, Do Thái)
- Thái (không khoảng cách từ)
- Hindi/Devanagari
- Emoji (multi codepoint, ZWJ)
- Dấu nối mềm (
­)
2. Đo lường Canvas
Các đoạn được đo qua Canvas measureText(), không kích hoạt reflow layout.
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello'); // Không reflow!
const width = metrics.width;
Kết quả được cache theo đoạn và font. Gọi lại với cùng văn bản/font sẽ dùng cache.
3. Bố cục toán học
layout() lấy cache chiều rộng từng đoạn, tính toán ngắt dòng bằng thuật toán tham lam:
- Cộng width cho tới khi vượt container
- Ngắt dòng mới
- Lặp lại
- Số dòng x lineHeight = tổng height
Không động đến DOM/Canvas, chỉ phép cộng, so sánh.
Handle tái sử dụng
prepare() trả về handle dùng lại cho mọi chiều rộng:
const handle = prepare(longArticleText, '16px "Inter"');
const mobile = layout(handle, 375, 24);
const tablet = layout(handle, 768, 24);
const desktop = layout(handle, 1200, 24);
Lý tưởng cho responsive UI: đo một lần, dùng nhiều nơi.
Các trường hợp sử dụng thực tế
1. Cuộn ảo với văn bản chiều cao động
Tích hợp Pretext.js vào virtual scroller:
import { prepare, layout } from '@chenglou/pretext';
interface TextItem {
id: string;
content: string;
}
function computeHeights(items: TextItem[], containerWidth: number) {
return items.map(item => {
const handle = prepare(item.content, '14px "Inter"');
const { height } = layout(handle, containerWidth, 20);
return { id: item.id, height: height + 32 }; // padding
});
}
// Đo 10.000 mục chỉ ~4ms
const heights = computeHeights(chatMessages, 600);
Không cần render DOM để đo, không lag, không "nhảy" UI.
2. Giao diện trò chuyện AI
Token stream liên tục, mỗi token có thể thay đổi số dòng. Đo lại chiều cao cực nhanh mỗi lần có token mới:
let streamedText = '';
const font = '15px "SF Pro"';
socket.on('token', (token: string) => {
streamedText += token;
const handle = prepare(streamedText, font);
const { height } = layout(handle, bubbleWidth, 22);
scroller.updateItemHeight(messageId, height + padding);
});
3. Bảng dữ liệu với cột văn bản
Xác định chiều rộng cột tự động:
function computeColumnWidth(values: string[], font: string, padding: number) {
let maxWidth = 0;
for (const value of values) {
const handle = prepare(value, font);
const { height } = layout(handle, Infinity, 20);
// Sử dụng max width từ handle (tùy thuộc vào API handle)
maxWidth = Math.max(maxWidth, /* chiều rộng đã tính */);
}
return maxWidth + padding;
}
4. Feed đa ngôn ngữ
Cùng API, chính xác cho mọi ngôn ngữ:
const posts = [
{ text: 'Thư viện này đã thay đổi mọi thứ', lang: 'en' },
{ text: 'Văn bản RTL với bố cục hai chiều chính xác', lang: 'ar' },
{ text: 'Văn bản CJK được ngắt ký tự đúng cách', lang: 'zh' },
];
posts.forEach(post => {
const handle = prepare(post.text, '16px system-ui');
const { height } = layout(handle, 400, 24);
});
Kiểm tra bố cục văn bản với Apidog
Khi xây dựng UI nặng văn bản dựa trên API, bố cục đúng chỉ là một nửa trận chiến. Bạn cần xác thực response API đúng định dạng, đúng tốc độ.
Apidog đơn giản hóa việc này: mô phỏng response API streaming để kiểm thử Pretext.js với các case đa dạng về độ dài, ngôn ngữ, Unicode, xác minh scroller hoạt động chính xác trước khi triển khai.
Đặc biệt hữu ích cho sản phẩm chat AI:
- Mô phỏng streaming: văn bản chia khối như output LLM thực
- Test đa ngôn ngữ: phát hiện lỗi layout sớm
- Validate schema: kiểm thử trường văn bản đúng định dạng
- Test tự động: bao phủ case biên về hiển thị văn bản
Lưu ý: một thư viện layout văn bản chỉ tốt nếu dữ liệu đầu vào tốt. API trả về "rác" thì layout cũng "rác", dù thư viện có nhanh cỡ nào.
Những hạn chế và phê bình đã biết
Trường hợp biên về độ chính xác
Một số phông chữ có kerning lạ, văn bản nhiều style, sai khác subpixel giữa Canvas và DOM, hoặc đặc điểm định hình riêng của browser có thể khiến Pretext.js lệch vài pixel so với layout thực.
Thường không ảnh hưởng tới virtual scroll (vài pixel không ai thấy), nhưng nếu bạn cần pixel-perfect typography thì cần cân nhắc.
Canvas không miễn phí
Mỗi lần prepare() vẫn gọi Canvas. Nếu app tạo hàng nghìn handle mới mỗi frame thì có thể bottleneck. Nên cache, batch khi có thể; thư viện không ép buộc.
Không hỗ trợ toàn bộ thuộc tính CSS
Pretext.js KHÔNG tính đến:
letter-spacingword-spacingtext-indenttext-transformfont-feature-settingsfont-variant
Nếu style phụ thuộc các thuộc tính trên, chiều cao tính ra sẽ lệch. Cần tự tính toán hoặc chấp nhận sai số.
Không thay thế render DOM
Pretext.js chỉ trả về chiều cao, không render văn bản. Bạn vẫn cần node DOM, Canvas hoặc SVG để hiển thị.
So sánh Pretext.js với cách truyền thống
| Tính năng | Pretext.js | Đo DOM | Ước lượng chiều cao |
|---|---|---|---|
| Tốc độ (1K mục) | ~2ms | ~94ms | ~0ms |
| Độ chính xác | Cao (Canvas) | Hoàn hảo | Thấp |
| Phụ thuộc DOM | Không (sau prepare) | Hoàn toàn | Không |
| Kích hoạt Reflow | Không | Mỗi lần đo | Không |
| Đa ngôn ngữ | Unicode đầy đủ | Đầy đủ | Kém |
| Hỗ trợ CSS | Hạn chế | Đầy đủ | Không |
| Bộ nhớ | Cache đoạn | DOM node | Tối thiểu |
| Bố cục responsive | 1 prepare, nhiều layout | Đo lại từng width | Ước lượng lại |
Chọn đúng phương pháp tùy yêu cầu: cần chính xác pixel & thuộc tính CSS? Dùng DOM. Cần tốc độ cho hàng nghìn item, chịu được sai số nhỏ? Dùng Pretext.js.
Bắt đầu
Cài đặt
npm install @chenglou/pretext
# hoặc
pnpm add @chenglou/pretext
# hoặc
bun add @chenglou/pretext
Sử dụng cơ bản
import { prepare, layout } from '@chenglou/pretext';
// Đo một đoạn văn
const handle = prepare(
'Pretext.js tính toán bố cục văn bản mà không cần chạm vào DOM.',
'16px "Inter"'
);
// Lấy chiều cao ở chiều rộng cụ thể
const result = layout(handle, 600, 24);
console.log(result.height); // ví dụ: 48
console.log(result.lineCount); // ví dụ: 2
Tích hợp với React
import { prepare, layout } from '@chenglou/pretext';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMemo, useRef } from 'react';
function VirtualChat({ messages }: { messages: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const containerWidth = 600;
const font = '14px "Inter"';
const lineHeight = 20;
const heights = useMemo(() => {
return messages.map(msg => {
const handle = prepare(msg, font);
const { height } = layout(handle, containerWidth, lineHeight);
return height + 24; // padding
});
}, [messages]);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: (index) => heights[index],
});
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
width: containerWidth,
}}
>
{messages[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
Giúp bạn có virtual chat với chiều cao item chính xác, không ước lượng, không lag, không reflow.
Sân chơi tương tác
Thử ngay tại pretextjs.dev/playground: dán văn bản, chọn font, chỉnh width, xem layout realtime — xác minh trước khi tích hợp.
Khi KHÔNG nên dùng Pretext.js
- Trang tĩnh, nội dung cố định: CSS đủ dùng, không cần thư viện.
- Bố cục yêu cầu in pixel-perfect: DOM đo chính xác hơn (Canvas có sai số nhỏ).
- Văn bản nhiều style CSS: Dùng các thuộc tính Pretext.js không hỗ trợ.
-
Server-side render: Cần polyfill Canvas (
node-canvas). Chưa chính thức hỗ trợ SSR. - Danh sách nhỏ (dưới 50 item): Đo DOM nhanh, không cần tối ưu hóa.
FAQ
Pretext.js đã sẵn sàng sản xuất chưa?
Đã phát hành từ 3/2026, hơn 14.000 sao GitHub, sử dụng ở Midjourney (production phục vụ hàng triệu user). Kiểm thử đa ngôn ngữ, nhiều case biên. Tuy nhiên, vẫn nên khóa version, kiểm thử với nội dung/font riêng.
Hỗ trợ React, Vue, Svelte?
Có. Pretext.js không phụ thuộc framework, chỉ gồm 2 hàm thuần TypeScript. Dùng mọi nơi, kể cả React hook, Vue composable, Svelte store hay JS thuần.
Xử lý font web thế nào?
prepare() đo theo font browser đã tải. Nếu font web chưa load, sẽ dùng fallback font, kết quả sai. Đảm bảo font đã sẵn sàng trước khi gọi, dùng document.fonts.ready để kiểm tra.
Có thể dùng để render Canvas/SVG?
Có. Dùng kết quả layout + điểm ngắt dòng để render lên Canvas 2D, WebGL, SVG hoặc DOM.
Hỗ trợ ngôn ngữ RTL?
Có. Pretext.js xử lý Ả Rập, Do Thái và RTL khác, kể cả text mixed direction.
Kích thước thư viện?
15KB (gzip), không phụ thuộc, chỉ dùng API browser chuẩn (measureText(), Intl.Segmenter nếu có).
Độ chính xác so với DOM?
Hầu hết case, Pretext.js lệch DOM 1-2px (tùy font, không tính các thuộc tính CSS đặc biệt). Với virtual scroll, sai số này chấp nhận được.
Đo văn bản nhiều style (bold, italic, size mix) được không?
Mỗi prepare() chỉ cho 1 font spec. Nếu có nhiều style, tự phân đoạn và gọi riêng cho từng đoạn. Sẽ cải thiện ở bản sau.
Kết luận
Pretext.js giải quyết triệt để bài toán đo văn bản nhanh, chính xác, không reflow DOM. Nếu bạn xây dựng virtual scroller, giao diện chat, bảng dữ liệu hoặc UI cần đo lường hàng nghìn khối text — hai hàm của Pretext.js có thể thay thế toàn bộ giải pháp tạm thời.
Không phải "viên đạn bạc": không hỗ trợ toàn bộ CSS, có sai số subpixel nhỏ, chưa sẵn sàng cho server-side. Nhưng cho bài toán đo trước chiều cao text với danh sách lớn, không gì sánh bằng.
Sẵn sàng tăng tốc UI nặng văn bản?
Kiểm thử API response của bạn với Apidog để đảm bảo lớp dữ liệu vững chắc, sau đó tích hợp Pretext.js vào flow render của bạn.


Top comments (0)