Привет! Меня зовут Костя Мамаев, я занимаюсь фронтенд-разработкой в поиске Яндекса. Некоторое время назад мы вместе с другими ребятами из команды помогали образовательным проектам компании. Среди прочего пришлось решить, казалось бы, простую задачку: отображать на экране и распечатывать на бумаге формулы, закодированные в популярном формате TeX. Звучит, как дело пяти минут, но в результате трёх подходов к снаряду появился полноценный микросервис для серверного рендеринга формул в svg и png. В статье расскажу, зачем мы пошли этим путём и почему ни один из существующих проектов не подошёл «из коробки».

Результаты нашей работы могут быть полезны и другим разработчикам, помогающим школьникам и учителям, поэтому готовый микросервис ждёт вас на гитхабе Яндекса. По ссылке весь джентльменский набор: Docker-контейнер, документация, открытый код.


В математических, физических и химических задачах (например, в материалах для подготовки к ЕГЭ и ОГЭ) много формул. Мы хотели, чтобы работать с ними было удобно. Качество отображения должно быть высоким, а формат — универсальным: мог выразить любую формулу, был простым в хранении и понятным за пределами Яндекса. Ещё важно, чтобы формулы выглядели одинаково и не «ехали» на разных экранах, а их обработка не перегружала CPU и расходовала энергию экономично — это особенно актуально для мобильных устройств.

Подход первый: фронтендерский. KaTeX

Показывать сырую TeX-разметку опрометчиво — вряд ли её сможет прочесть пользователь без кандидатской степени, поэтому сначала нужно её приготовить. Первое решение, которое приходит на ум — конвертировать на стороне клиента, будь то что-то встроенное или библиотека для рендеринга TeX.

Немногие вспомнят, что в браузерах уже есть поддержка математических формул — MathML. Но, как показало беглое исследование, формат давно забыт и пользователями, и вендорами браузеров. Пришлось двигаться дальше. Оказалось, выбор инструментов не так уж велик: монструозный, но всеядный и расширяемый MathJaX или относительно молодой и быстрый KaTeX. Как говорится, из двух зол — выбирай меньшее. Решили попробовать KaTeX.

Плюсы:

  • Realtime-отрисовка формул. Пользователь пишет формулу и сразу видит, что будет на выходе.

  • Формирование разметки на клиентской стороне — не нужно ничего хранить.

Минусы:

  • Каким бы легковесным ни был KaTeX по сравнению MathJaX, он всё равно оставался слишком большим по стандартам Яндекса.

  • Артефакты realtime-генерации: мигание на странице и долгий рендеринг, особенно на мобильных устройствах.

  • Проблемы при печати (о них поговорим чуть позже).

  • KaTeX отдаёт html-разметку, так что скопировать формулу как формулу не получится.

  • Двойная работа при использовании SSR. Сначала генерируем html на сервере, а потом на клиенте.

Подход второй: браузерный. Puppeteer

Результат, полученный с KaTeX, не устроил — хотелось избавиться от недостатков первой реализации. Пришла идея сделать всё на сервере без realtime-генерации формул. В этом случае можно выбрать более мощную библиотеку — MathJaX. Она умеет «из коробки» превращать TeX в разметку, MathML и svg. Здорово, не правда ли? Оставалось написать обвязку в виде сервера, парочки web-хуков для добавления заданий на обработку и сложить всё в s3. Это оказалось не так просто. Возникли проблемы с печатью:

  • выяснилось, что svg не копируется через ctrl+c ctrl+v в Microsoft Word,

  • ломалось выравнивание формул в тексте.

Мы много общались с учителями, репетиторами, готовящими школьников к ЕГЭ и ОГЭ, и преподавателями вузов. Большинство из них распечатывает на бумаге материалы к занятиям. Это подтверждали интервью, проведённые коллегами: все респонденты-репетиторы рассказывали, что печатают задания для учеников. Значит, закрывать глаза на эти проблемы нельзя.

Здесь пригодился Puppeteer. Мы добавили конвертацию из svg в png, чтобы реализовать копирование в Word. А это, стоит заметить, один из самых востребованных сценариев среди преподавателей. Но, как только появился браузер, сервис стал тормозить: генерация одной формулы занимала секунды, а пересоздание всех формул (у нас их было 110 469) — дни или даже недели.

Подход третий: серверный. MathJax, sharp и немного смекалки

Чтобы получить решение, которое работает быстро, не нагружает даже слабенькие устройства и даёт приемлемое качество изображения, оставалось решить две проблемы:

  • ускорить генерацию png,

  • починить выравнивание формул.

Первая проблема решалась просто, хотя пришлось перепробовать множество вариантов, чтобы получить нужный результат. Puppeteer был безжалостно выпилен, а вместо него — добавлена маленькая и быстрая библиотека sharp. Да, решение породило внешнюю зависимость, но сервис запускается в Docker-контейнере, так что это было приемлемо. Скорость генерации заметно выросла.

А вот с «поехавшими» формулами в тексте пришлось побороться. Было предположение, что выравнивание по центру или базовой линии поможет, но оно работало хорошо только для однострочных или идеально симметричных формул. MathJax сам генерирует правильные отступы в svg-файлах (правда, в ex, но это не беда), но для оптимизации отрисовки выносит одинаковые символы в defs. Они конфликтуют между вставленными svg, и выглядит это, будто сломалась головка струйного принтера. Дополнительный минус — раздутая разметка и невозможность повторного использования одинаковых svg на странице. Обращение к png выглядело жестом отчаяния: проблемы примерно те же, а качество графики хуже.

Решение оказалось достаточно простым:

  1. генерируем svg-файл,

  2. получаем из него атрибуты со стилями (это же обычный xml, не так ли?),

  3. удаляем атрибуты,

  4. генерируем png из svg,

  5. сохраняем в метаинформации файла данные о стилях, нужных для корректного отображения картинки.

Готово!

Результаты и выводы

Теперь формулы копируются, масштабируются, лениво загружаются, идеально выровнены, а CPU пользователя не нагревается до состояния сковородки. Удалось добиться скоростных улучшений. Размер десктопных и тачевых бандлов уменьшился на 64.5кб (37%).

Обработка страницы варианта ЕГЭ, на которой нужно было отрендерить 94 формулы:

  • с использованием Katex: 7594 ms

  • без Katex: 6584 ms

  • финальный вариант: 1010 ms

Эта же страница, но на слабом CPU и с медленным 3G:

  • с использованием Katex: 40229 ms

  • без Katex: 37524 ms

  • финальный вариант: 2705 ms

Чему я научился?

  • Даже самая тривиальная задача может оказаться сложнее, чем представлялось вначале.

  • Svg не копируется через буфер обмена.

  • Конвертировать svg в png с помощью браузера — это долго.

  • Не стоит забывать о метаинформации. 

Надеюсь, этот проект принесёт пользу и другим разработчикам, помогающим школьникам, студентам и учителям. Если вы один из них — встретимся на гитхабе.