ملخص سريع
Pretext.js هي مكتبة TypeScript خالية من التبعيات لقياس وتخطيط النصوص متعددة الأسطر باستخدام عمليات حسابية بحتة بدون أي تفاعل مع DOM. توفر أداء أسرع بنحو 500 مرة من getBoundingClientRect()، وتدعم جميع أنظمة الكتابة الرئيسية. إذا كنت تبني متصفحات افتراضية أو واجهات دردشة أو شبكات بيانات، فهذه المكتبة تحل مشكلة قياس النص التي ما زالت المتصفحات تتجاهلها منذ عقود.
مقدمة
عند استدعاء getBoundingClientRect() أو قراءة offsetHeight في JavaScript، يتوقف المتصفح ويقوم بإعادة حساب التخطيط، مما يؤدي إلى "إعادة تدفق متزامنة قسرية" (Forced Synchronous Reflow) وهي عملية مكلفة جدًا.
مثال: إذا كان لديك 1000 رسالة دردشة أو 10000 صف في جدول بيانات وتحتاج إلى قياس كل عنصر، سيتم استهلاك موارد كبيرة وإبطاء تجربة المستخدم.
💡Apidog: فرق التطوير التي تبني واجهات تعتمد على API تدرك هذه المعاناة. بث البيانات إلى واجهات ديناميكية مع الحفاظ على الأداء مهمة صعبة عندما يعيقك محرك التخطيط.
Cheng Lou (مطور react-motion ومساهم رئيسي في React وReasonML) بنى Pretext.js لحل هذه المعضلة. أُطلقت المكتبة في مارس 2026 ووصلت بسرعة إلى أكثر من 14,000 نجمة على GitHub.
هذه المقالة تعرض لك طريقة عمل Pretext.js، وكيفية استخدامها عمليًا، ومتى تصلح لحل مشكلتك.
ما هو Pretext.js؟
Pretext.js هو محرك تخطيط نصي بالـ JavaScript/TypeScript يقيس النصوص باستخدام العمليات الحسابية فقط، دون أي تفاعل مع DOM أو إعادة تدفق.
واجهة الاستخدام الأساسية:
import { prepare, layout } from '@chenglou/pretext';
// التحضير (مرة واحدة فقط لكل نص وخط)
const handle = prepare('Hello, pretext.js', '16px "Inter"');
// التخطيط لأي عرض حاوية (حسابات رياضية فقط)
const { height, lineCount } = layout(handle, 400, 24);
-
prepare(): حساب وتخزين مقاييس النص (يستخدم Canvas مرة واحدة فقط). -
layout(): حساب تخطيط النص لأي عرض حاوية وارتفاع سطر دون الرجوع للمتصفح.
لماذا هذا مهم لتطبيقات API الكثيفة؟
عند بناء تطبيقات تعتمد على استجابات API متدفقة (مثل مساعدين الذكاء الاصطناعي أو لوحات البيانات أو المحررين التعاونيين)، تحتاج لمعرفة ارتفاع النص قبل عرضه. قياس DOM سيُبطئ الأداء، بينما Pretext.js يعطيك النتائج في أجزاء من ميكروثانية.
المشكلة التي يحلها Pretext.js
شرح إعادة التدفق المتزامنة القسرية
كل استدعاء مثل:
const elements = document.querySelectorAll('.text-block');
elements.forEach(el => {
const height = el.getBoundingClientRect().height; // إعادة تدفق!
});
يجبر المتصفح على إعادة حساب التخطيط لكل عنصر. في قائمة كبيرة، سيؤدي هذا إلى بطء ملحوظ.
مشكلة التمرير الافتراضي
مكتبات مثل react-window تحتاج معرفة ارتفاع كل عنصر. إذا كان الارتفاع متغيرًا يجب قياسه، غالبًا يتم عرض العناصر خارج الشاشة ثم قياسها، وهو ما يهزم الغرض من التمرير الافتراضي.
Pretext.js يسمح لك بحساب ارتفاع النص بدقة قبل إنشاء أي عقدة DOM.
أرقام الأداء
| الأسلوب | 1,000 كتلة نصية | 500 كتلة نصية |
|---|---|---|
DOM (getBoundingClientRect) |
~94 ميلي ثانية | ~47 ميلي ثانية |
Pretext.js (layout()) |
~2 ميلي ثانية | ~0.09 ميلي ثانية |
| فرق السرعة | أسرع بحوالي 47 مرة | أسرع بحوالي 500 مرة |
كيف يعمل Pretext.js داخليًا
المرحلة 1: تجزئة النص
prepare() تطبّق قواعد Unicode لكسر الأسطر، وتجزئ النص حسب اللغة (CJK، العربية، الإيموجي، إلخ).
المرحلة 2: قياس Canvas
لكل جزء، تستخدم المكتبة Canvas.measureText() للقياس (بدون إعادة تدفق):
const ctx = offscreenCanvas.getContext('2d');
ctx.font = '16px "Inter"';
const metrics = ctx.measureText('Hello');
const width = metrics.width;
المرحلة 3: تخطيط حسابي
layout() تجمع عروض الأجزاء وتقارنها بعرض الحاوية، وتحدد فواصل الأسطر حسابيًا فقط.
معالج قابل لإعادة الاستخدام
يمكنك استخدام ناتج prepare() لقياس النص على أي عرض شاشة مباشرة:
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. التمرير الافتراضي مع نصوص بارتفاع متغير
احسب ارتفاع كل رسالة دردشة بسرعة:
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 }; // الحشو
});
}
const heights = computeHeights(chatMessages, 600);
2. واجهات الدردشة المدعمة بالذكاء الاصطناعي
تحديث ارتفاع فقاعة الدردشة مع كل رمز يصل:
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. شبكات البيانات مع أعمدة نصية
تقدير حجم عمود بناءً على محتواه النصي:
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 إن وجد
maxWidth = Math.max(maxWidth, /* computed width */);
}
return maxWidth + padding;
}
4. خلاصات المحتوى متعددة اللغات
تعمل بنفس الـ API لجميع اللغات:
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
عند بناء واجهات تعتمد على النصوص وواجهات API، تحتاج لاختبار استجابات الـ API والتأكد من صحة تنسيق البيانات وسرعتها.
يوفر Apidog أدوات لمحاكاة استجابات API متدفقة واختبار تكامل Pretext.js مع حالات نصية متعددة. يمكنك:
- محاكاة استجابات البث (streaming).
- اختبار حمولات متعددة اللغات.
- التحقق من مخططات الاستجابة.
- تشغيل مجموعات اختبار آلية لحالات حافة النص.
البيانات السيئة تؤدي لتخطيطات سيئة، حتى مع محركات قياس نص قوية.
القيود والانتقادات المعروفة
حالات الحافة لدقة العرض
قد يختلف عرض النص في بعض الحالات (مثل خطوط ذات kerning خاص، أو نص بأسطر مختلطة الحجم، أو اختلافات Canvas/DOM) عن التخطيط الفعلي للمتصفح ببضع بكسلات.
قياس Canvas ليس مجانيًا
استدعاء prepare() بكميات ضخمة بدون تخزين مؤقت قد يؤثر على الأداء في بعض الحالات. احرص على التخزين المؤقت إذا كنت تتعامل مع نصوص متكررة.
عدم دعم خصائص CSS معقدة
Pretext.js يعتمد فقط على مواصفات الخط. خصائص مثل letter-spacing, word-spacing, text-indent, إلخ غير مدعومة بشكل مباشر.
لا يعرض النص فعليًا
Pretext.js يحسب فقط الارتفاع وعدد الأسطر. يجب عليك عرض النص بنفسك (DOM أو Canvas أو SVG).
Pretext.js مقابل الأساليب التقليدية
| الميزة | Pretext.js | قياس DOM | ارتفاعات مقدرة |
|---|---|---|---|
| السرعة (1000 عنصر) | ~2 ميلي ثانية | ~94 ميلي ثانية | ~0 ميلي ثانية |
| الدقة | عالية (Canvas) | مثالية (DOM) | منخفضة |
| الاعتماد على DOM | فقط في prepare()
|
كامل | لا شيء |
| إعادة التدفق | صفر | واحد لكل قياس | صفر |
| دعم لغات متعددة | كامل Unicode | كامل (DOM) | ضعيف |
| دعم خصائص CSS | محدود (الخط فقط) | كامل | لا شيء |
| استهلاك الذاكرة | أجزاء مخزنة | عقد DOM | أقل ما يمكن |
| تخطيطات متجاوبة |
prepare() مرة، layout() متعددة |
قياس لكل عرض | تقدير لكل عرض |
إذا كنت تحتاج لدقة بكسل مثالية ودعم CSS كامل، استخدم قياس DOM. أما لو كنت تحتاج سرعة عبر آلاف العناصر مع تقبل فروقات طفيفة، استخدم Pretext.js.
البدء
التثبيت
npm install @chenglou/pretext
# أو
pnpm add @chenglou/pretext
# أو
bun add @chenglou/pretext
الاستخدام الأساسي
import { prepare, layout } from '@chenglou/pretext';
// قياس فقرة
const handle = prepare(
'Pretext.js يحسب تخطيط النص دون لمس DOM.',
'16px "Inter"'
);
// الحصول على الارتفاع لعرض معين
const result = layout(handle, 600, 24);
console.log(result.height); // مثال: 48 (سطران × 24 بكسل)
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;
});
}, [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>
);
}
ساحة اللعب التفاعلية
يمكنك تجربة Pretext.js مباشرة على pretextjs.dev/playground: الصق نصك، اختر الخط، وعدل العرض وشاهد النتائج فورًا.
متى لا تستخدم Pretext.js؟
- صفحات ثابتة: إذا كان النص لا يتغير ولا تحتاج افتراضية، لا داعي لاستخدام المكتبة.
- تخطيط مثالي بالبكسل: إذا كانت فروقات البكسل الفرعي مهمة، استخدم قياس DOM.
- تنسيق CSS معقد: إذا كنت تعتمد على خصائص CSS نصية غير قياسية.
- العرض من جانب الخادم: تحتاج إلى polyfill لـ Canvas في Node.js.
- قوائم صغيرة وثابتة: إذا كانت القائمة صغيرة جدًا، التحسين غير ضروري.
الأسئلة الشائعة
هل Pretext.js جاهز للإنتاج؟
نعم، لكنه إصدار جديد. اختبره جيدًا مع خطوطك ومحتواك.
هل يعمل مع React/Vue/Svelte؟
نعم، المكتبة لا تعتمد على إطار معين.
كيف يتعامل مع خطوط الويب؟
تأكد من تحميل الخط قبل استدعاء prepare(). استخدم document.fonts.ready عند الحاجة.
هل يمكن استخدام Pretext.js مع Canvas أو SVG؟
نعم، استخدم القيم المحسوبة لتخطيط النص أينما تريد.
هل يدعم اللغات من اليمين إلى اليسار؟
نعم، يدعم العربية والعبرية وكل النصوص ثنائية الاتجاه.
ما هو حجم الحزمة؟
15 كيلوبايت بعد التصغير، بدون تبعيات.
ما مدى دقته مقارنة بقياس DOM؟
لأغلب النصوص، الدقة تصل إلى 1-2 بكسل فرق. مع خصائص CSS الخاصة قد تزيد الفروقات.
كيف يقيس النص المنسق (غامق/مائل/أحجام مختلطة)؟
كل استدعاء prepare() يقيس نمط خط واحد فقط. للنصوص المنسقة، جزئ النص وقس كل جزء على حدة.
الخلاصة
Pretext.js توفر قياس نص سريع وفعال بدون إعادة تدفق DOM، وتحل مشاكل افتراضية القوائم والدردشات والشبكات النصية بواجهتين فقط. ليست حلاً سحريًا لكل سيناريو، لكنها الأفضل لحساب ارتفاعات نصوص كثيفة قبل العرض.
هل أنت مستعد لبناء واجهات نصية أسرع وأكثر استجابة؟ ابدأ باختبار نقاط نهاية API باستخدام Apidog ثم دمج Pretext.js في خط أنابيب العرض الخاص بك.


Top comments (0)