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

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



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


    Начиналось все с того, что в ResumUP мы рисуем 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.

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



    Пользователь делает запрос к 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 в минимизированном виде).



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

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

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

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

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



    Спасибо за внимание, с вами были somebody32 и Terminal
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 18
    • 0
      Круто!
      • 0
        А зачем генерировать всё это каждый раз заново, а не использовать картинки?
        • 0
          можно например сделать интерактивными детали резюме, с картинками / генерацией «имадж мэпов» (пусть даже сделанных при помощи стилей) это гораздо менее гибко
          • 0
            Ну при изменении данных картинки надо как-то перегенирировать и делать это быстро
          • 0
            Спасибо за очень интересный пост. А есть ли Baileys и Absinth на гитхабе?
            • +2
              есть, но, к сожалению, код пока закрыт, в скором времени обязательно откроем
              • 0
                это скорое время пока не наступило? ;)
            • 0
              Как-то всё сложно…
              • 0
                Ну SVG — он тормозной, да. Не зря GitHub его не использует в своих графиках в пользу Canvas.
                Можно было отрисовать в Canvas, а для редких маргиналов, браузеры которых не поддерживают его отдавать картинки, отрисованную нодой при помощи node-canvas и того же кода.
                • 0
                  Тормозил не SVG, тормозил js. SVG рисуется моментально
                  • +1
                    SVG не тормозной, просто для определённых задач наличие DOM — это плюс, а для других — минус. И Canvas, и SVG замечательные технологии со «страшными» API.
                  • 0
                    А чего не взяли phantom.js?
                    • 0
                      Тогда его ещё не было. Ну и это не очень изящное решение, на мой взгляд.
                      • 0
                        Этот проект с 2010 года существует, вы были раньше?
                        Почему решение не изящное?
                        • 0
                          потому что втыкать рафаэль в ноду и упорно пытаться играть в браузер на стороне сервера не кажется изящным решением. Тем более, что с подобного подхода мы наши эксперименты и начали
                          • 0
                            Без ноды можно обойтись: code.google.com/p/phantomjs/wiki/Interface
                            Задача по сути нагенерить кучу статических картинок. Нагенерили и ладно. Не вижу смысла для такой задачи столько велосипедов строить.
                    • 0
                      Было бы интересно посмотреть насколько изменится скорость отрисовки при переходе на Raphaël 2.1.0
                      • 0
                        Перешли на Raphael 2.1. На MacBookAir 2011 i5, рисуется все так же долго. Около 3-х секунд. Сервер выдает картинку за 100мс.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое