DEV Community

Cover image for Pretext.js: ไลบรารี 15KB เร่งความเร็วการจัดวางข้อความ 500 เท่า
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

Pretext.js: ไลบรารี 15KB เร่งความเร็วการจัดวางข้อความ 500 เท่า

สรุปสั้นๆ (TL;DR)

Pretext.js เป็นไลบรารี TypeScript แบบ zero-dependency สำหรับการวัดและจัดตำแหน่งข้อความหลายบรรทัดโดยใช้คณิตศาสตร์ล้วนๆ ไม่ต้องพึ่ง DOM หรือ getBoundingClientRect() มันเร็วกว่าเดิม ~500 เท่า และรองรับระบบเขียนหลักทั่วโลก เหมาะสำหรับ Virtual Scroller, Chat UI และ Data Grid ที่ต้องการวัดข้อความจำนวนมากอย่างรวดเร็ว

ทดลองใช้ Apidog วันนี้

บทนำ

ทุกครั้งที่คุณใช้ 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

pretext.js demo

หลักการ: แทนที่จะถาม 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);
Enter fullscreen mode Exit fullscreen mode

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

ทุกครั้งที่เรียก getBoundingClientRect():

  1. JS หยุดชั่วคราว
  2. ล้าง pending style
  3. คำนวณ layout ใหม่ทั้งเอกสาร หรือ subtree
  4. ส่งคืนค่า

ถ้าวนลูปวัด 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;
Enter fullscreen mode Exit fullscreen mode

ขั้นตอนที่ 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);
Enter fullscreen mode Exit fullscreen mode

กรณีการใช้งานจริง

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

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

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

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

การทดสอบเลย์เอาต์ข้อความกับ Apidog

เมื่อสร้าง UI ข้อความที่ขับเคลื่อนด้วย API — จัด layout ได้ครึ่งทาง อีกครึ่งต้องตรวจสอบว่า API ส่งข้อมูลถูกต้อง รวดเร็ว และรูปแบบเหมาะสม

apidog test

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

การใช้งานพื้นฐาน

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

ผสานกับ 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

สนามทดลองแบบ 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)