DEV Community

Cover image for Pretext.js: Thư Viện 15KB Tối Ưu Bố Cục Văn Bản Nhanh Gấp 500 Lần
Sebastian Petrus
Sebastian Petrus

Posted on • Originally published at apidog.com

Pretext.js: Thư Viện 15KB Tối Ưu Bố Cục Văn Bản Nhanh Gấp 500 Lần

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.

Dùng thử Apidog ngay hôm nay

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.

Pretext.js demo

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);
Enter fullscreen mode Exit fullscreen mode

Chỉ với 2 hàm:

  • prepare() đo và cache các đoạn văn bản (gọi Canvas measureText()).
  • 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ị...
});
Enter fullscreen mode Exit fullscreen mode

Mỗi lần gọi getBoundingClientRect():

  1. Tạm dừng JavaScript
  2. Flush tất cả thay đổi style
  3. Tính lại layout toàn trang
  4. 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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Cộng width cho tới khi vượt container
  2. Ngắt dòng mới
  3. Lặp lại
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 kiểm thử

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-spacing
  • word-spacing
  • text-indent
  • text-transform
  • font-feature-settings
  • font-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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)