Привет! Меня зовут Костя Мамаев, я занимаюсь фронтенд-разработкой в поиске Яндекса. Некоторое время назад мы вместе с другими ребятами из команды помогали образовательным проектам компании. Среди прочего пришлось решить, казалось бы, простую задачку: отображать на экране и распечатывать на бумаге формулы, закодированные в популярном формате 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 выглядело жестом отчаяния: проблемы примерно те же, а качество графики хуже.
Решение оказалось достаточно простым:
генерируем svg-файл,
получаем из него атрибуты со стилями (это же обычный xml, не так ли?),
удаляем атрибуты,
генерируем png из svg,
сохраняем в метаинформации файла данные о стилях, нужных для корректного отображения картинки.
Готово!
Результаты и выводы
Теперь формулы копируются, масштабируются, лениво загружаются, идеально выровнены, а 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 с помощью браузера — это долго.
Не стоит забывать о метаинформации.
Надеюсь, этот проект принесёт пользу и другим разработчикам, помогающим школьникам, студентам и учителям. Если вы один из них — встретимся на гитхабе.