สรุปสั้นๆ (TL;DR)
Pretext.js เป็นไลบรารี TypeScript แบบ zero-dependency สำหรับการวัดและจัดตำแหน่งข้อความหลายบรรทัดโดยใช้คณิตศาสตร์ล้วนๆ ไม่ต้องพึ่ง DOM หรือ getBoundingClientRect() มันเร็วกว่าเดิม ~500 เท่า และรองรับระบบเขียนหลักทั่วโลก เหมาะสำหรับ Virtual Scroller, Chat UI และ Data Grid ที่ต้องการวัดข้อความจำนวนมากอย่างรวดเร็ว
บทนำ
ทุกครั้งที่คุณใช้ getBoundingClientRect() หรืออ่านค่า offsetHeight ใน JavaScript เบราว์เซอร์จะหยุดทุกอย่าง ล้างสไตล์ที่ยังไม่ประมวลผล คำนวณ layout ใหม่ และ re-render เรียกว่า Forced Synchronous Reflow — หนึ่งในกระบวนการที่กินทรัพยากรที่สุดในเบราว์เซอร์
ลองจินตนาการว่าต้องวัด 1,000 ฟองแชท หรือ 10,000 แถวใน data grid เฟรมจะตก แอปจะกระตุก และผู้ใช้อาจคิดว่าแอปเสีย
💡 ทีม Apidog ที่สร้าง frontend ขับเคลื่อนด้วย API เข้าใจดีว่าการสตรีมข้อมูล API สู่ UI แบบไดนามิกให้ลื่นไหลตลอดเวลานั้นเป็นเรื่องยาก เมื่อ layout engine ของเบราว์เซอร์กลายเป็นอุปสรรค
เฉิง โหลว (Cheng Lou) ผู้สร้าง react-motion และผู้ร่วมพัฒนา React/ReasonML ที่ Meta ได้สร้าง Pretext.js เพื่อแก้ปัญหานี้ ไลบรารีนี้เปิดตัวในเดือนมีนาคม 2026 ได้รับดาว GitHub กว่า 14,000 ภายในไม่กี่วัน และสร้างกระแสใน Hacker News
บทความนี้จะอธิบายว่า Pretext.js คืออะไร, ทำงานอย่างไร, ควรใช้เมื่อไหร่ และข้อจำกัดที่ควรรู้ เพื่อให้คุณตัดสินใจเลือกใช้ได้อย่างเหมาะสม
Pretext.js คืออะไร?
Pretext.js คือ text layout engine แบบ pure JavaScript/TypeScript วัดและจัดตำแหน่งข้อความด้วยคณิตศาสตร์ล้วนๆ ไม่มี getBoundingClientRect(), ไม่มี offsetHeight, ไม่มี reflow, ไม่มี thrashing
หลักการ: แทนที่จะถาม browser ว่า "ข้อความนี้สูงเท่าไหร่?" (ซึ่งบังคับให้ render ก่อน) Pretext.js จะคำนวณเองโดยใช้ font metrics จาก Canvas API
ตัวอย่าง API:
import { prepare, layout } from '@chenglou/pretext';
// 1. เตรียมข้อความ (ครั้งเดียว, แคชได้)
const handle = prepare('Hello, pretext.js', '16px "Inter"');
// 2. คำนวณ layout ที่ความกว้างใดก็ได้ (pure math)
const { height, lineCount } = layout(handle, 400, 24);
prepare() วัดและแคชข้อความ 1 ครั้ง (ใช้ Canvas measureText()), layout() คำนวณ pure math ล้วนๆ ไม่มี DOM เพิ่มเติม
ทำไมจึงสำคัญสำหรับแอปที่ใช้ API หนัก
ถ้าคุณสร้างแอปที่ต้องสตรีม API response เช่น AI assistant, dashboard แบบ real-time, หรือ collaborative editor — คุณต้องรู้ความสูงของข้อความก่อน render ถ้าไม่มีก็จะเจอ virtual scroller กระตุก, chat bubble กระโดด, UX แย่
Pretext.js ให้ผลลัพธ์ในระดับไมโครวินาที ไม่ใช่มิลลิวินาที — ต่างกันชัดเจนเมื่อ scale ขึ้น
ปัญหาที่ Pretext.js แก้ไข
Forced Synchronous Reflow คืออะไร?
const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
const height = el.getBoundingClientRect().height; // REFLOW!
// ใช้ height...
});
ทุกครั้งที่เรียก getBoundingClientRect():
- JS หยุดชั่วคราว
- ล้าง pending style
- คำนวณ layout ใหม่ทั้งเอกสาร หรือ subtree
- ส่งคืนค่า
ถ้าวนลูปวัด 1000 elements = reflow 1000 ครั้ง ≈ 94ms (6 เฟรมตกที่ 60fps)
Virtual Scrolling มีปัญหาอะไร?
Library อย่าง react-window หรือ tanstack-virtual ต้องรู้ความสูงแต่ละ item เพื่อคำนวณ scroll ถ้าเนื้อหาข้อความสูงแปรผัน ต้อง render offscreen เพื่อวัด หรือประมาณค่า ซึ่งเกิด jump หรือจำกัดฟีเจอร์
Pretext.js คำนวณความสูงล่วงหน้าแบบ exact ได้เลย ไม่ต้องสร้าง DOM node สักตัว
ตัวเลขจริง
| วิธี | 1,000 blocks | 500 blocks |
|---|---|---|
DOM (getBoundingClientRect) |
~94ms (6 เฟรมตก) | ~47ms |
Pretext.js (layout()) |
~2ms | ~0.09ms |
| Speed diff | ~47x faster | ~500x faster |
Pretext.js ทำงานอย่างไร?
ขั้นตอนที่ 1: Text Segmentation
prepare() จะ normalize ข้อความ, ใช้ Unicode line-breaking rules, แบ่งเป็น segment (เช่น ตัวอักษร, word, emoji, ฯลฯ) รองรับภาษา CJK, RTL, ไทย, ฮินดี, emoji, soft hyphens ครบ
ขั้นตอนที่ 2: วัดด้วย Canvas
แต่ละ segment วัดด้วย Canvas measureText() (ไม่มี reflow) และ cache ไว้
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello');
const width = metrics.width;
ขั้นตอนที่ 3: Layout (Pure Math)
layout() รับ segment width + container width แล้ว greedy break line, คูณ line count ด้วย line-height ได้ความสูง
ไม่มี DOM, ไม่มี Canvas ซ้ำ — แค่บวก/เปรียบเทียบ
Reusable Handle
prepare() คืน handle ที่นำไป layout() ที่ความกว้างใดก็ได้ — เหมาะกับ responsive design
const handle = prepare(longArticleText, '16px "Inter"');
const mobile = layout(handle, 375, 24);
const tablet = layout(handle, 768, 24);
const desktop = layout(handle, 1200, 24);
กรณีการใช้งานจริง
1. Virtual Scrolling (ข้อความสูงแปรผัน)
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 }; // +32 สำหรับ padding
});
}
const heights = computeHeights(chatMessages, 600); // 10,000 รายการ ~4ms
2. AI Chat Interface (streaming)
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. Data Grids (ข้อความในคอลัมน์)
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);
// ใช้ handle เพื่อ track width จริง (สมมติ API handle มี width)
// maxWidth = Math.max(maxWidth, handle.width);
}
return maxWidth + padding;
}
4. ฟีดหลายภาษา
const posts = [
{ text: 'This library changed everything', lang: 'en' },
{ text: 'RTL text with correct bidirectional layout', lang: 'ar' },
{ text: 'CJK text gets proper character-level breaks', lang: 'zh' },
];
posts.forEach(post => {
const handle = prepare(post.text, '16px system-ui');
const { height } = layout(handle, 400, 24);
});
การทดสอบเลย์เอาต์ข้อความกับ Apidog
เมื่อสร้าง UI ข้อความที่ขับเคลื่อนด้วย API — จัด layout ได้ครึ่งทาง อีกครึ่งต้องตรวจสอบว่า API ส่งข้อมูลถูกต้อง รวดเร็ว และรูปแบบเหมาะสม
Apidog ช่วยให้คุณจำลอง API response แบบ streaming, สร้าง test case หลากหลายภาษา/ข้อความ, ตรวจสอบ schema และรัน test อัตโนมัติ ครอบคลุมขอบเขต Unicode ต่างๆ ก่อน deploy จริง
- จำลอง streaming response
- ทดสอบ multi-language payload
- ตรวจสอบ schema
- รันชุด test อัตโนมัติ
ข้อมูล API ที่ดี = layout ที่ดี — ไม่ว่าคุณจะวัดเร็วแค่ไหน ถ้า data ไม่ดี layout ก็ผิดได้
ข้อจำกัดและข้อวิจารณ์
ความแม่นยำเป๊ะยังไม่ได้ 100%
บางกรณี (เช่น kerning พิเศษ, ขนาดฟอนต์ผสม, subpixel rendering, quirks ของแต่ละ browser) อาจมี error 1-2px — ใช้ virtual scroll ไม่เห็นผล แต่ถ้าต้อง pixel-perfect อาจไม่เหมาะ
prepare() ยังต้อง Canvas
ถ้าสร้าง handle ใหม่หลายพันครั้งต่อเฟรม อาจเจอคอขวด — ควรแคชและ batch เอง
ไม่รองรับ CSS properties บางตัว
ไม่คำนึงถึง letter-spacing, word-spacing, text-indent, text-transform, font-feature-settings, font-variant ถ้าใช้ style เหล่านี้ ความสูงอาจคลาดเคลื่อน
ไม่แทนที่การ render DOM
Pretext.js แค่บอก "ข้อความนี้จะสูงเท่าไหร่" — ไม่ได้ render ข้อความจริง ต้องใช้ DOM/Canvas/SVG เอง
เปรียบเทียบกับวิธีเดิม
| คุณสมบัติ | Pretext.js | วัด DOM | ประมาณ |
|---|---|---|---|
| ความเร็ว (1,000 รายการ) | ~2ms | ~94ms | ~0ms |
| ความแม่นยำ | สูง (Canvas) | สมบูรณ์แบบ | ต่ำ |
| พึ่งพา DOM | ไม่มีหลัง prepare | เต็ม | ไม่มี |
| Trigger reflow | 0 | 1/วัด | 0 |
| หลายภาษา | Unicode เต็ม | เต็ม | ไม่ดี |
| รองรับ CSS | จำกัด | เต็ม | ไม่มี |
| Memory | แคชข้อความ | DOM node | ต่ำ |
| Responsive layout | 1 prepare, multi layout | วัดใหม่ทุกขนาด | ประมาณใหม่ |
เลือกตาม use case ถ้าเน้นความเร็ว scale ใหญ่ virtual scroll — Pretext.js เหนือกว่า ถ้าต้อง pixel perfect และ CSS เต็ม — ใช้ DOM
เริ่มต้นใช้งาน
ติดตั้ง
npm install @chenglou/pretext
# หรือ
pnpm add @chenglou/pretext
# หรือ
bun add @chenglou/pretext
การใช้งานพื้นฐาน
import { prepare, layout } from '@chenglou/pretext';
const handle = prepare(
'Pretext.js computes text layout without touching the DOM.',
'16px "Inter"'
);
const result = layout(handle, 600, 24);
console.log(result.height); // เช่น 48 (2 บรรทัด x 24px)
console.log(result.lineCount); // เช่น 2
ผสานกับ 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>
);
}
สนามทดลองแบบ Interactive
ลองเล่นกับ pretextjs.dev/playground — วางข้อความ เลือกฟอนต์ ปรับความกว้าง ดู layout แบบ real-time
เมื่อใดที่ไม่ควรใช้ Pretext.js
- เว็บ static, content ไม่เปลี่ยน, ไม่ virtualize — CSS ทำงานได้ดี
- ต้องการ pixel-perfect print layout — error subpixel สำคัญ
- สไตล์ข้อความด้วย CSS หนักมาก — ใช้ letter-spacing, text-indent ฯลฯ
- SSR (server-side render) — Pretext.js พึ่ง Canvas API ใน browser (Node.js ต้อง polyfill)
- รายการเล็ก (เช่น 50 รายการ) — วัด DOM ใช้เวลา <5ms
FAQ
Pretext.js ใช้งาน production ได้ไหม?
เปิดตัว มี test ครอบคลุมภาษา/edge case เยอะ แต่ยังใหม่ — ควร lock version และทดสอบกับฟอนต์/เนื้อหาจริงของคุณ
ใช้กับ React, Vue, Svelte ได้ไหม?
ได้ เป็น pure TypeScript library — ใช้ใน React hooks, Vue composables, Svelte stores หรือ JS ปกติได้หมด
จัดการ Web Font ยังไง?
prepare() ใช้ฟอนต์ที่ browser โหลด ณ ตอนนั้น ถ้า webfont ยังไม่ load จะวัดผิด — ต้องรอ font loaded (document.fonts.ready) ก่อน
ใช้กับ Canvas/SVG render ได้ไหม?
ได้ ใช้ layout ที่คำนวณไปจัดตำแหน่ง text ใน Canvas, WebGL, SVG หรือ DOM ได้หมด
รองรับ RTL (ขวาไปซ้าย) ไหม?
รองรับอาหรับ, ฮีบรู, RTL และข้อความผสมทิศทาง
Bundle size?
15KB minified ไม่มี dependency ใดๆ ใช้แค่ Canvas API กับ Intl.Segmenter
ความแม่นยำเทียบ DOM?
ส่วนใหญ่ตรงกับ DOM ใน 1-2px ถ้าใช้ letter-spacing, word-spacing จะคลาดเคลื่อนมากขึ้น เหมาะ virtual scroll ที่ error เล็กๆ มองไม่เห็น
วัดข้อความสไตล์ผสม (bold, italic, size) ได้ไหม?
prepare() รับฟอนต์เดียวต่อ call ถ้าข้อความมีหลาย style ต้องแบ่งเองแล้ว prepare ทีละ segment
สรุป
Pretext.js แก้ปัญหาการวัดข้อความเร็ว/แม่นยำโดยไม่มี DOM reflow เหมาะกับ virtual scroller, chat UI, data grid ที่ต้องวัดข้อความจำนวนมาก เปลี่ยนจากเทคนิคเดิมเป็นแค่ 2 ฟังก์ชัน
ไม่ใช่ยาครอบจักรวาล — ไม่รองรับ CSS property ทั่วไป, มี error subpixel, ยังไม่รองรับฝั่ง server แต่ใน use case หลัก (virtual list) ไม่มีอะไรเทียบได้
พร้อมสร้าง UI ข้อความที่เร็วกว่า? เริ่มจากตรวจสอบ API endpoint ของคุณด้วย Apidog ให้มั่นใจว่าข้อมูลพร้อม แล้วนำ Pretext.js ไปใช้ในขั้นตอน render


Top comments (0)