28 марта 2026 года инженер Midjourney Cheng Lou выложил в открытый доступ библиотеку, которая за неделю набрала почти 40 тысяч звёзд на GitHub. И имя ей — Pretext. Это движок текстовой верстки на чистом TypeScript, который полностью обходит DOM и браузерный layout рефлоу. За этим стоит вполне ощутимая проблема и красивое решение.

Давайте разберемся, что это такое, зачем оно нужно, как устроено и стоит ли тащить к себе в проект.
Проблема: почему текст — это боль
Midjourney стримит AI‑контент в реальном времени: токены приходят, текст растет, а интерфейсу нужно постоянно знать — какой высоты сейчас текстовый блок, нужно ли скроллить, сколько места занимает сообщение?
Классический способ узнать размеры текста в браузере это положить его в DOM и спросить у браузера через getBoundingClientRect или offsetHeight, но проблема в том, что это запускает layout reflow — синхронную блокирующую операцию, при которой браузер пересчитывает позиции и размеры всех затронутых элементов на странице. И для статических страниц это нормально, но когда у вас чат с AI, где каждую секунду прилетают новые токены, и вам нужно знать высоту каждого сообщения для визуализации — рефлоу заставляет вашу машину попотеть: фреймрейт падает, интерфейс дергается, батарея садится — вы нервничаете.

А теперь представьте masonry‑сетку из тысяч текстовых карточек. Или редактор документов. Или коллаборативную доску. Каждое измерение текста это поход в DOM, а каждый поход — это рефлоу. Бдыщ.
Идея: а что если не ходить в DOM вообще?
Ключевая мысль Pretext такая: измерение текста — это же арифметика, а не операция DOM.
Если мы один раз замерим ширину каждого слова (через Canvas API, который не запускает reflow), то дальше можем вычислять переносы строк и высоту текстового блока чистой математикой: без DOM, без reflow и без блокировок.
Звучит очевидно, но естественно все не так просто: нужно корректно обрабатывать Unicode, переносы строк для CJK‑текста, арабскую вязь с right‑to‑left, эмодзи, мягкие переносы, режимы white-space и word-break. Автор сказал, что‑то типо «я прошел через адские глубины», чтобы довести это до production‑качества. И я ему верю. И, кстати, поделился что юзал для этой задачи вайб‑кодинг технику с помощью Claude и Codex .
Решение: две фазы вместо одной
Ключевая идея Pretext — разделить задачу на два этапа.

Фаза 1: prepare() — запускается один раз для каждой пары «текст + шрифт». Внутри происходит следующее: текст нормализуется (пробелы, юникод), разбивается на сегменты с помощью Intl.Segmenter (это стандартный браузерный API для корректного разбиения по словам в любой локали), применяются правила склейки (неразрывные пробелы, мягкие переносы), каждый сегмент измеряется через Canvas measureText(), результаты кэшируются.
Этот шаг стоит дорого — порядка 17–19 мс на батч из 500 текстов. Но как понятно из названия — сделать его можно один раз и больше не беспокоится об этой проблеме.
Фаза 2: layout() — чистые вычисления поверх закэшированных ширин — никаких обращений к DOM. Принимает результат prepare(), ширину контейнера и высоту строки и возвращает высоту блока и количество строк, при этом занимает ~0.09–0.10 мс для тех же 500 текстов.
Вот откуда «500x». layout() вызывается при каждом ресайзе, при каждом новом токене, при любом изменении ширины. prepare() — как уже говорил, только один раз, когда текст появляется. Да, сравнение нечестное получается: если учесть стоимость prepare(), общее ускорение куда скромнее. Но в реальном сценарии, если один и тот же текст пересчитывается десятки раз (ресайз, стриминг, виртуализация), выигрыш — действительно есть.
Как выглядит в коде
Минимальный пример — это всего четыре строчки:
import { prepare, layout } from '@chenglou/pretext' const prepared = prepare('Ваш текст здесь', '16px Inter') const { height, lineCount } = layout(prepared, containerWidth, 24)
Готово. height получается это предсказанная высота блока в пикселях. lineCount — это количество строк. И все это без единого обращения к DOM.
Если нужно больше контроля то есть prepareWithSegments() и layoutWithLines(), которые отдают информацию о каждой строке (текст, ширина, курсоры начала/конца). А для самых продвинутых сценариев можем использовать layoutNextLineRange(), который позволяет рендерить текст построчно, обтекая произвольные фигуры:
import { layoutNextLineRange, materializeLineRange, prepareWithSegments } from '@chenglou/pretext' const prepared = prepareWithSegments(article, '16px Inter') let cursor = { segmentIndex: 0, graphemeIndex: 0 } let y = 0 while (true) { // Строки рядом с картинкой — уже, остальные — на всю ширину const width = y < image.bottom ? columnWidth - image.width : columnWidth const range = layoutNextLineRange(prepared, cursor, width) if (range === null) break const line = materializeLineRange(prepared, range) ctx.fillText(line.text, 0, y) cursor = range.end y += 26 }
Получаем полный программный контроль над обтеканием.
Что поддерживается
Pretext — это уже довольно серьезно проработанная библиотека с точки зрения интернационализации. Поддерживаются латиница, кириллица, CJK (китайский, японский, корейский), арабский, иврит, тайский, кхмерский, хинди, эмодзи, а также смешанный текст с bidirectional‑направлением (скажем, английский и арабский вперемешку). Поэтому демо или вайбкодинг библиотекой не назовешь.
Весит ~15 КБ в gzip. Зависимостей — ноль (такое мы любим). Работает с React, Vue, Svelte, Angular, ванильным JS. Работает в браузерах, Node.js, Deno, Bun, Cloudflare Workers, Web Workers.
Где это применимо
Возможно кого‑то ввел в заблуждение, поэтому стоит уточнить: Pretext не заменяет DOM‑рендеринг. Текст по‑прежнему живет в DOM — его можно выделять, копировать, он доступен скринридерам, его видит браузерный поиск. Pretext заменяет измерение текста — вот и все.
Практические сценарии, где это действительно имеет значение:
Виртуализация списков. У вас чат или лента с тысячами сообщений разной длины. Для виртуального скролла нужно знать высоту каждого элемента до его рендеринга. Стандартный подход: рендерить в скрытый контейнер, измерить, удалить. С Pretext считаем арифметически и получаем быстрый и отличный результат. Работает с React Virtuoso, TanStack Virtual и любой другой библиотекой виртуализации.
Стриминг AI‑ответов. Собственно, исходный юзкейс из Midjourney: токены прилетают, текст растет, высота блока известна заранее и как итоге нет layout shift, нет дерганий интерфейса.
Shrinkwrap для чат‑баббл. CSS width: fit-content подгоняет ширину под самую длинную строку, но когда последняя строка короткая, остается пустое место. В CSS нет свойства «найди минимальную ширину для ровно N строк», а Pretext считает это математически и результат: более плотные, аккуратные бабблы.
Респонсивные layout'ы без рефлоу. Один вызов prepare(), дальше три вызова layout() с разными ширинами для mobile/tablet/desktop и того ноль обращений к DOM.
Креативная типографика. Текст, обтекающий 3D‑объекты, magazine‑style layout'ы с переменными колонками и pull‑цитатами, ASCII‑арт, текст, который ведет себя как частицы. Все это требует построчного контроля, который CSS дать не может. В сети уже много красивых примеров.

Ограничения и минусы
Надо и про минусы рассказать, куда без них.
Pretext — не полный layout engine. Он работает только с текстом. Если вам нужен layout всей страницы — на этом его полномочия все.
Шрифт должен быть загружен до вызова prepare(). Если шрифт ещё не прилетел — метрики будут неверными. Нужно дождаться document.fonts.ready или аналога.
system-ui небезопасен. На macOS этот шрифт может привести к неточным результатам. Нужно указывать конкретный именованный шрифт.
Молодой проект. Версия молодая, API может меняться. В issues на GitHub видно, что сообщество активно находит корнер‑кейсы. Для прототипов и внутренних инструментов — вполне можно использовать прямо сейчас, а для продакшена с критически важным текстовым рендерингом стоит внимательно тестировать на своих данных.
Это не серверный рендеринг. prepare() использует Canvas API для измерения шрифтов. На сервере без браузера полноценно это не работает (хотя в репозитории есть экспериментальный HarfBuzz‑бэкенд для headless‑окружений).
И тут ИИ-агенты
Отдельно хочу подсветить одну деталь. В README автор прямо пишет, что API спроектирован «AI‑friendly». Что это значит?
Когда AI‑агент генерирует интерфейс — не фиксированную страницу, а динамический UI, который меняется на каждом шаге, то браузерный рефлоу становится тяжелым местом. Интерфейс не заверстан заранее, он собирается на лету и каждый апдейт‑ это измерение, пересчет, задержка.
С Pretext агент может заранее рассчитать layout чистой математикой, без обращения к DOM. Это быстрее, предсказуемее и легче интегрируется в программный pipeline. Для вайбинтерфейсов это одназначно плюс.
Как попробовать
Установка:
npm install @chenglou/pretext
Минимальный пример для предсказания высоты:
import { prepare, layout } from '@chenglou/pretext' // Подготовка: один раз при появлении текста const prepared = prepare('Привет, Хабр! Это тестовый текст.', '16px Inter') // Layout: вызывается при каждом ресайзе / изменении ширины const { height, lineCount } = layout(prepared, 300, 24) console.log(`Высота: ${height}px, строк: ${lineCount}`)
Для вывода построчной информации:
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext' const prepared = prepareWithSegments('Длинный текст...', '16px Inter') const lines = layoutWithLines(prepared, 400, 24) lines.forEach(line => { console.log(`"${line.text}" — ширина: ${line.width}px`) })
Живые демо можно посмотреть на chenglou.me/pretext — там аккордеоны, чат‑баббли, editorial layout с многоколоночным обтеканием, ASCII‑арт и сравнение алгоритмов переноса (жадный, Кнут‑Пласс, CSS justification). Сообщество также делает демо.
Стоит ли тащить в свой проект
Зависит от контекста.
Если у вас обычный лендинг или корпоративный сайт с фиксированным контентом вам Pretext, скорее всего, не нужен. CSS справляется отлично, и добавлять лишнюю зависимость ради несуществующей проблемы не стоит (только если нужно показать видимость работы).
Если у вас текстово‑тяжелое приложение (чат, редактор, лента, дашборд с карточками), и вы уже сталкиваетесь с проблемами виртуализации, и с подтормаживанием при массовом ресайзе — Pretext решает именно эту боль. 15 КБ, ноль зависимостей, framework‑agnostic. А если вы строите что‑то с AI‑стримингом или генеративным UI — это, пожалуй, первый кандидат на интеграцию.
Полезные ссылки
GitHub репозиторий — исходный код, README, документация
npm пакет —
npm install @chenglou/pretextОфициальные демо — аккордеоны, чат-баббли, editorial layout, ASCII-арт
Демо от сообщества — эксперименты от разработчиков
DeepWiki: Pretext — подробный разбор архитектуры
Статья на DEV.to — хороший критический взгляд на то, что в Pretext реально важно
Twitter автора — Cheng Lou, обновления и контекст
Надеюсь тебе понравилось. Лучшая благодарность — это твоя подписка на мой Telegram-канал ?
Комментарии (15)

cmyser
09.04.2026 20:07Вау, никогда такого не было и вот опять придумали как не взаимодействовать с dom

Alexandroppolus
09.04.2026 20:07А если держать на странице отдельный iframe с минимумом верстки, и измерять текст внутри него - это тоже будет долгий layout рефлоу?

JerryI
09.04.2026 20:07Да, подобное делали в невидимом летающем контейнере. Но рефлоу даже внутри него мелкого тормоза. Странно конечно, что canvas всё равно выигрывает. Возможно потому, что там не "честные" глифы рисуют. Как уже сказали раньше, если был бы API к reflow механизму страницу, то все проблемы бы решились без канвасов

Alexandroppolus
09.04.2026 20:07Странно конечно, что canvas всё равно выигрывает. Возможно потому, что там не "честные" глифы рисуют.
canvas вообще не рисует, у него есть метод, который просто измеряет размеры строки в пикселях, но там нельзя например задать ограничение по ширине, то есть никакого выравнивания он не делает. Потому и быстрый.

inikonzs
09.04.2026 20:07он перестанет быть долгим даже просто на скрытом абсолюте, он не будет афектить ничего. А канвас выигрывает да, за счет отсутствия самого факта рендера

Sap_ru
09.04.2026 20:07Есть стойкое ощущение, что эта проблема давным давно решена без вот этого велосипеда. Просто нужно размеры кэшировать, а не всё заново пересчитывать. Текст-то только растёт.

meowpointerexception
09.04.2026 20:07В реальных проектах те же проблемы обычно закрываются батчингом обновлений и снижением частоты reflow

inikonzs
09.04.2026 20:07читал, всё время думал, ну canvasRenderingContext2d.measureText на то есть. И да, он самый и есть оказалось)

Dren0r
09.04.2026 20:07кроме поиграться с дракончиком - никакой пользы от библиотеки нет. Все UI проектируются блоками, а не обтеканием текста вокруг какого-то объекта. Любой генеративный контент нормально рендерится без всяких библиотек.
Подобрая верстка может помочь только при газетных колонках, и то все было решено через float еще 20 лет назад, и сейчас не используется
Lezvix
09.04.2026 20:07Посмотри пример с чатом, там как раз демонстрируется максимально компактное уложение текста в блоки, для эффективного расходования пространства

Dren0r
09.04.2026 20:07и как часто в генеративном контенте двигается размер блока чата?
блок статичен, а меняется количество текста в нем, которое растет просто вниз.
Все вариации его отрисовки давно готовы в css - text-wrap, overflow-wrap, word-break, white-space, hyphens, text-overflow, line-clamp, line-break. Придумали настроек уже на любой случай жизни.
Эта библиотека просто красивый пет проект поиграться, практического применения там в лучшем случае на 1% каких-то задач, которые даже придумать не получается
Alexandroppolus
История про то, как внутри браузера реализована некая довольно полезная функциональность, но по каким-то причинам до сих пор нет нормального браузерного api к ней, и пришлось дублировать реализацию. Не в первый раз, кажется.
Akuma
На этой логике весь веб работает :)