Сложный SVG на клиенте и сервере

в 10:03, , рубрики: javascript, node.js, nodejs, raphaeljs, ruby on rails, svg, метки: , , , ,

Это небольшая история страданий, боли, взлетов и падений в попытках ускорить работу RaphaelJS на больших и сложных SVG. Если вы страдаете от подобных проблем, то не стоит ждать в конце этой статьи серебряной пули, но, надеюсь, что про наш путь поиска решения будет интересно прочитать всем.

С чего все начиналось?

Начиналось все с того, что в ResumUP мы рисуем svg, рисуем его много и красиво.

Сложный SVG на клиенте и сервере

Исторически сложилось так, что все это рисуется с помощью прекрасной библиотеки RaphaelJS (тут нет иронии, спасибо Дмитрию Барановскому за нее). Как только началась работа над проектом, то Raphael был единственным решением, которое позволило во вменяемые сроки реализовать все то, что нарисовали дизайнеры.

Но радость наша была преждевременной: отрисовка резюме занимала в среднем 5-7 секунд на нормальных машинах в нормальных браузерах (при переходе одного из условий в состояние «ненормальный» — до 30! секунд). Понятно, что в долгосрочной перспективе это никого не устраивало и нужно было искать способы оптимизации.

Часть 1. (Танго и) cache

Итак, что мы имеем?

  • кучу js-кода, которая рисует красивые картинки
  • время исполнения от 5 до 30 секунд
  • немного времени и много желания ускорить это все

Первое, что всем приходит на ум, — посмотреть, почему рафаэль рисует все так долго. Небольшое погружение в код библиотеки открыло интересные особенности относительно того, как браузеры работают c svg, деление вышло всего на два лагеря: те, кто понимают inline svg и те, что нет.
Так вот, для совместимости со старыми браузерами рафаэль работает напрямую с DOM-дерево документа (это все очень грубо, но фактически так), понятно, что когда элементов много, то даже нормальным браузерам приходится тяжко (а если вспомнить, что при отрисовке еще и логика есть, то совсем плохо становится)

Вооружившись знаниями и осознав, что быстро мы рафаэль поправить не сможем, решили пойти в обход и кешировать результат отрисовки на стороне сервера. Придумали — сделали, схема получилась такой:

  1. Человек заходит на резюме
  2. Каждый из 10 блоков отрисовывается и отправляется на сервер
  3. При повторном заходе на резюме, блок уже вставляется прямо в шаблон на стороне сервера, рафаэль даже не вызывается
  4. Если какого-то блока нет в кеше, то отрисовывается только он

Это позволило сократить загрузку страницы до секунды, что все равно было довольно много, но казалось неимоверно быстрым после 5-7. Единственная проблема — браузеры без поддержки inline svg, они рисовали все и всегда. (тестировали мы возможности браузера с помощью modernizr)

Часть 2. Рисуем svg на сервере. Плачевный опыт

Итак, а теперь что мы имеем?

  • кучу js-кода, которая рисует красивые картинки
  • время исполнения от 1 до 5-30 секунд
  • немного времени и бизнес необходимость рисовать svg на сервере

Так как страсть к оптимизации бесконечна, то мы решили не останавливаться на текущей системе кеширования и решить проблему как-то более кардинально. И тут перед нами встает задача от начальства изучить возможность рисования наших резюме и вакансий на сервере.

Первым шагом я полез смотреть, что в мире руби есть для этого (бекэнд у нас на рельсах), но не нашел ничего вразумительного. Да даже если бы и нашел, то не было совершенно никакого желания переписывать всю ту кучу кода, что отвечает у нас за отрисовку блоков.

Т.е. появилось первое обязательное требование — оставить текущий js-код, рисующий блоки.

Самое первое решение, что мы решили попробовать — запустить все под rubyracer, обертка вокруг v8, позволяющая в руби запускать js-код.
Но проблема в том, что v8 — машина для интерпретации js, но не браузера, и, соответственно, DOM-дерева в ней нет, поэтому рафаэль упирался, но рисовать ничего не хотел. Но мы далеко не первые, кто столкнулся с задачей имитации браузера, поэтому быстро нагуглиги решение — jsdom

Схема взлетела, но при попытке отрисовать всю базу резюме мы свалились уже на 10-м с ошибкой could not allocate memory. Посмотрели статистику, замерили расход памяти и получили около 50-70 мегабайт утечки и 8 секунд на исполнение. Но сам факт: использовать существующий код для отрисовки SVG на сервере можно, так что мы остались довольны.

Поэтому решили упростить немного схему и выкинуть руби. Вспомнив о главном тренде и объекте насмешек 2011 года — nodejs, тем более, что вся схема и так работала под v8.

Воткнув этого франкештейна в expressjs, мы, хоть и со скрипом, но получили нашу картинку. Скорость увеличилась до 4-5 секунд, утечка упала до 40-50.
После долгого профилирования пришли к выводу, что единственный способ избавиться от утечки — избавиться от jsdom. Но для этого требовался совершенно иной подход.

Часть 3. Продолжаем рисовать SVG в браузере. Первая надежда

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

Основная идея состояла в том, что мы хотели разделить процесс отрисовки SVG на два этапа:

  • Создание абстрактной модели, которая описывала бы будущую картинку
  • Конвертирование этой модели в SVG

Этот подход имел несколько преимуществ:

  • Решили проблему с отсутсвием DOM в nodejs и смогли рисовать резюме на сервере
  • Для браузеров с поддержкой inline svg мы теперь могли генерировать SVG как текст, а потом напрямую вставлять его в документ, что стало бы серёзным ускорением быстродействия
  • Обеспечили возможность в будущем генерировать с помощью различных адаптеров разные представления для нашей модели (например, VML для IE8)

Сначала появилась идея распотрошить raphael и вытащить из него всю логику, которая не завязана на создание DOM-элементов, а остальное попытаться изменить. Однако, после нескольких часов внимательного изучения исходников версии 2.0, стало ясно, что это работа на неделю, дедлайн же стремительно приближался.

Подумав, мы решили написать свою реализацию, скопировав интерфейс raphael. Дальше все просто: каждый блок на резюме представлял собой отдельный класс, в который, в зависимости от окружения, прокидывали либо raphael, либо инстанс нашего велосипеда.

Так родился baileys.

В итоге получилось:

Сложный SVG на клиенте и сервере

Пользователь делает запрос к rails, rails стучится к nodejs, nodejs вызывает код блоков резюме и отсылает рельсам json с данными об SVG, а рельсы, в свою очередь, должны преобразовывать json в SVG, так и родился absinthe.

Схема заработала, но до production-решения было еще далеко: отрисовка занимала в среднем 0,5 секунды и возникало много вопросов с масштабированием.

Часть 4. Нет предела совершенству

Прототип был, но хотелось довести его до ума, поэтому мы решили составить список требований, который бы удовлетворил нас на текущий момент:

  • Единство кода: хотелось, чтобы код, рисующий на стороне сервера, никак не отличался от кода, работающего на стороне клиента
  • Хотелось минимизировать работу ruby-части и вынести всю отрисовку SVG целиком вне рамок основного проекта

В итоге получилось, что из основного репозитория мы выделили еще 4 проекта:

  1. Tetris — весь код отрисовки блоков, абстрагированный от библиотеки, с помощью которой он будет рисоваться
  2. Baileys — копирует интерфейс raphael, формирует json-описание будущих svg-объектов. Мы его сильно переписали, переделали работу функции getBBox, и вообще заметно ускорили.
  3. Absinthe — решили, что нет смысла больше держать его на стороне руби-сервера, переписали все на coffeescript и собрали npm-пакет, адаптировав под понимание json'а, отданного baileys’ом
  4. Polyomino — серверное приложение на expressjs, получает запрос и данные от основного приложения, вызывает baileys и absinthe, отдает результат.

В итоге мы получили возможность использовать tetris как на стороне ноды в виде npm-пакета, так и на стороне рельс, подключив в asset pipeline (для этого просто собрали простой gem, который несет с собой tetris+baileys+absinthe в минимизированном виде).

Сложный SVG на клиенте и сервере

Текущие бенчмарки показывают, что мы рисуем резюме в среднем за 70мс, что уже вполне неплохой результат, если вспомнить про первоначальные 7 секунд.

В дальнейших наших планах стоит перенос всей системы отрисовки на связку baileys’а и absinthe’а и полный отказ от raphael. Для тех же браузеров, что не поддерживают inline svg отдавать img с указанием на SVG, что отрисовано на сервере.

В случае если nodejs по каким-то причинам не отвечает, всегда можно переключиться в режим отрисовки SVG на клиенте.

Так же мы вынашиваем светлые мысли открыть код baileys и absinthe и поделиться наработками со всеми желающими, вдруг кому понадобится.

Если вы дошли до конца, то вам бонус — талисман нашей команды:

Сложный SVG на клиенте и сервере

Спасибо за внимание, с вами были somebody32 и Terminal

Автор: Somebody32

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js