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

Star history
Star history

Давайте разберемся, что это такое, зачем оно нужно, как устроено и стоит ли тащить к себе в проект.

Проблема: почему текст - это боль

Midjourney стримит AI-контент в реальном времени: токены приходят, текст растет, а интерфейсу нужно постоянно знать - какой высоты сейчас текстовый блок, нужно ли скроллить, сколько места занимает сообщение?

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

masonry-сетка
masonry-сетка

А теперь представьте 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). Сообщество также делает демо на somnai-dreams.github.io/pretext-demos.

Стоит ли тащить в свой проект

Зависит от контекста.

Если у вас обычный лендинг или корпоративный сайт с фиксированным контентом вам Pretext , скорее всего, не нужен. CSS справляется отлично, и добавлять лишнюю зависимость ради несуществующей проблемы не стоит (только если нужно показать видимость работы).

Если у вас текстово-тяжелое приложение (чат, редактор, лента, дашборд с карточками), и вы уже сталкиваетесь с проблемами виртуализации, и с подтормаживанием при массовом ресайзе - Pretext решает именно эту боль. 15 КБ, ноль зависимостей, framework-agnostic. А если вы строите что-то с AI-стримингом или генеративным UI - это, пожалуй, первый кандидат на интеграцию.

Полезные ссылки

Надеюсь тебе понравилось. Лучшая благодарность - это твоя подписка на мой Telegram-канал 😊