Оптимизация производительности фронтенда

Тормозящий сайт — это боль не только пользователя, но и разработчика. Как можно исправить ситуацию,  в каких случаях нужно делать ставку на кэширование, а где можно довериться процессору, и как все это может помочь оптимизировать производительность сложного фронтенд-приложения, на практике готов объяснить эксперт по JS и преподаватель Академии HTML Игорь Алексеенко. Под катом — расшифровка его доклада с Frontend Conf 2017.

98ef875aa8c4e0ba7ec73f076594a534.jpg


О спикереe69e60df38a3a3a66f74b586a373cd9d.jpg
Игорь Алексеенко — разработчик с большим стажем, ведет базовый и продвинутый курсы по JS  в Академии HTML. Работал в Студии Лебедева, Островке и JetBrains.

Сегодня я хотел поговорить о проблеме разработчиков. Я поделюсь своей собственной болью, но я надеюсь, что вы разделяете ее.

940589d9164ea40230b54bbe8685e5e3.png

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

9d6048db65d39238b7a48d0398a92f11.png

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

d36af3c1c26df9df7bfbd83c83a33a7f.png

Ладно, когда просто тормозит интерфейс. Ну, подумаешь, пользователь придет в нашу онлайн-игру, но не сможет прицелиться и убить соперника или увидит какую-нибудь дерганую анимацию.

593300ee8988ca25d5cab2c56d678813.png

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

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

Поэтому в целом тормозящий сайт — это серьезная проблема. И поэтому ее нужно решать.
Но для того, чтобы решать проблему, нужно знать врага в лицо. Давайте разберемся, что же такое вообще тормоза на сайтах, почему сайты тормозят и откуда это берется.

80bdbd02b0c98d3c9277ea31719283bc.png

Тормоза на сайтах возникают, когда взаимодействие с пользователем перестает быть ровным. Что это значит? Дело в том, что когда пользователи просматривают сайт, они видят не просто какую-то статическую картинку. Потому что сайт — это не просто картинка, это это процесс взаимодействия пользователя с интерфейсом, который мы ему предлагаем.

5128bc1d8ab24538d0d7c87b51d6bca3.png

Пользователь может увидеть те же самые анимации, о которых я говорил. Он может банально скроллить сайт — и это тоже динамическое взаимодействие. Он тыкает на кнопки, вводит текст, перетаскивает элементы. Это все работает динамически.

88e581047995d142d1fb908e3296a572.png

Почему это работает динамически? Почему сайты могут жить продолжительное время?
Это происходит из-за того, что во все движки браузеров встроена такая конструкция, как Event Loop.

93450c7b532c49d92e28cd86d1189536.png

На самом деле Event Loop — это такой простой программистский прием, который заключается в том, что мы просто запускаем бесконечный цикл с какой-то определенной частотой. Так чтобы у нас не забился стек и была какая-то производительность.

Этот бесконечный цикл на каждом шаге проверяет внешние условия и запускает определенные действия. Например, он понимает, что пользователь прокрутил мышку и нужно немного сместить страницу.

67644b0439303521da40c471b26e12e5.png

С Event Loop, который встроен в движок браузера, синхронизированы все взаимодействия с пользователем — прокрутки, прочие штуки и код, который мы выполняем в долгосрочной перспективе. То есть все зависит от этого Event Loop.

Как попадать в кадры Event Loop?

У нас есть цикл, который крутится с определенной частотой. Но мы, как фронтендеры, не можем контролировать эту частоту и не знаем ее. У нас есть лишь возможность пользоваться уже готовыми, предназначенными для нас кадрами. То есть частоту мы не контролируем, но вписаться в нее можем. Для этого есть конструкция requestAnimationFrame.

Если мы передаем код в callback requestAnimationFrame, то попадаем в начало очередного кадра обновления. Кадры обновления бывают разные. На MAC, например, эти кадры стараются вписаться в 60 Fs, но частота не всегда бывает равной 60 Fs. Дальше на примерах я это покажу.

2075cf89cdc88b5996cf0e8efcdfbc1d.png

Мы уже разобрали, что в основе Java-скрипта лежит бесконечный цикл, который обновляется со временем. Теперь можно предположить, откуда могут возникать тормоза.

Есть кадры, которые со временем обновляются. Мы запускаем на сайте какие-то вычисления. Любые. Все, что мы пишем в Java-скрипте, — это фактически вычисления. Если эти вычисления занимают дольше одного видимого кадра обновления, пользователь видит лаги.

ee399350707cb7565b55b418ebcff711.png

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

d7af3735bd372df1b7d372c3781ad29f.png

Или вычисления могут стать слишком большими, и тогда страница зависнет и с ней ничего нельзя будет делать.

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

492244bfeb8e23a4b92526eb5c2ae46d.png

Мы — программисты, поэтому у нас есть инструменты, на которых запускается наш код. Главная задача программиста — правильно распределить баланс нагрузки между процессором и памятью.
Все мы знаем, что такое процессор. Это устройство в компьютере, которое отвечает за мгновенные вычисления. То есть любая команда, которую мы пишем, преобразуется в инструкцию для процессора, и он будет эту команду выполнять.

Но если есть последовательность команд, которая приводит к какому-то большому результату (допустим, мы посчитали сложное значение), но не хотим повторять эту последовательность — мы можем записать результат работы этой команды в память и пользоваться другой инструкцией процессора, которая называется чтением из памяти.

Об этом я как раз и хотел поговорить.

cb77a61835cac31f5fc563cc45650820.png

Первая оптимизация, которая видится логичной, — использовать память для того, чтобы не использовать вычисления.

В принципе такая стратегия звучит выигрышно. Более того, это хорошая стратегия и она уже используется.

a511cbc6a314a71da728c86818e24860.png

Например, в Java-скрипте есть встроенный объект Math, который предназначен для работы с математикой и вычислениями. Этот объект содержит не только методы, но и некоторые посчитанные популярные значения — чтобы их не пересчитывать. Например, как в случае числа π, которое сохранено до определенного знака.

Во-вторых, есть хороший пример про старые времена. Я очень люблю программистов 80-х годов потому, что они писали эффективные решения. Железо было слабое, и им приходилось придумывать какие-то хорошие штуки.

В 3D-шутерах всегда используется тригонометрия: для того, чтобы считать расстояния и синусы, косинусы и прочие подобные штуки. С точки зрения компьютера это тоже достаточно дорогостоящие операции.

Раньше программисты на этапе компиляции засовывали таблицы синусов и косинусов прямо в программный код. То есть они пользовались уже посчитанными значениями тригонометрических функций. Вместо того, чтобы их рассчитывать для отрисовки сцены, они брали их как константы.
Таким образом можно оптимизировать вообще все на свете. Можно заранее посчитать анимацию — как она будет выглядеть, и сделать все что угодно.

В принципе звучит очень круто.

4d54fd2a35a52a9fbfd908217ca637a1.png

Смотрите, вот кадры, и вместо того, чтобы запускать вычисления, которые занимают несколько кадров, мы запускаем вычисления, которые занимаются только чтением: прочитали готовое значение, подставили, использовали. И получается, что интерфейс работает очень быстро.
Теоретически это звучит очень круто. Но давайте подумаем о том, как именно фронтендеры работают с памятью.

107b373bf185512f6b40987f2b251191.png

Когда мы открываем вкладку браузера, нам выделяется какой-то определенный объем памяти. Его мы, кстати, тоже не знаем. В этом мы тоже ограничены.

Но мало того — мы не можем этой памятью управлять.

Есть еще особенность — в этой памяти уже что-то хранится:

  1. В этой памяти хранится ран-тайм языка: все конструкторы, все функции. Сам язык хранится в памяти;
  2. Вы тоже пользуетесь какими-то данными: скачиваете что-то с Аякса, генерируете какие-то структуры;
  3. У вас есть DOM-дерево, и оно тоже попадает в память, потому что Java-скрипт не умеет читать HTML, и браузер преобразовывает для него разметку в набор объектов, в дерево. В память браузера, во вкладку попадает все, что есть в разметке, в том числе все теги в виде каждого отдельного объекта. Попадают тексты.


То есть для каждого переноса, который стоит между тегами, создается объект памяти браузера, как text-mode, и он висит.
Мы видим, что у нас и так куча всего в памяти, но при этом хотим записать туда что-то свое. Это достаточно опасно, потому что память может начать тормозить.

14bfc6f0ec94ae1cf9467bcdf4b125a5.png

Есть две основных причины — и, как ни странно, они противоречат друг другу. Первая называется «Сборка мусора», а вторая — «Отсутствие сборки мусора».

7f5dfb2906921a461d3d0428dd8b5ae1.png

Разберем каждую из них.

Перед тем как я объясню, что такое сборка мусора, я расскажу, как мы будем смотреть на работу с точки зрения производительности, памяти и вообще всего на свете.

30db985d57c335ee2c9b2490910e95af.png

Дело в том, что все эти вещи можно измерить. В любом браузере есть инструменты разработчика. Я буду показывать на примере Chrome, но в других браузерах это тоже есть. Мы будем смотреть вкладку «Профилирование» или «Perfomance».

Ее в любой момент можно открыть и замерить так называемый снэпшот производительности браузера. Нажимаем на кнопку «Record/записать». Производится запись производительности, и через какое-то время можно посмотреть на то, что происходило на странице, и как это влияло на память и процессор.

Из чего состоит эта вкладка?

  • Во-первых, сверху есть тайм-лайн, которая показывает секунды, то есть время жизни вкладки.
  • Fps — частота кадров, которая была в тот момент времени;
  • Загруженность процессора — на совместном графике он показывает разные вычисления;
  • Скриншоты. Можно их, кстати, не показывать. Советую их отключать, потому что если вы записываете профиль производительности со включенными скриншотами, у вас гарантированно проседает Fps. То есть если вам нужно посчитать именно Fps, выключите скриншоты на всякий случай.
  • Память. Это самый нижний график.
  • Детальная статистика той же самой информации, которую мы видим сверху. То есть можно увеличить в любой момент профайлеры и посмотреть именно по кадрам, что происходило. Мы можем увеличить даже до кадра и посмотреть, какие операции на нем выполнялись, — даже с ссылками на код.


Итак, эту производительность мы будем мерять на Instagram с котиками. Все любят котиков, все любят Instagram — поэтому я решил сделать так.

1ceab6f6054c434d1e3a580d7a2bc98c.png

Котиков не бывает много, поэтому мы будем смотреть большие страницы. У нас будет пять страниц с 5 000 котиков, и мы научимся их переключать.

Ниже код, с помощью которого я генерирую котиков. Они все уникальные.

8e4566f29d939f0f5b04ba4161855265.png

То есть я создаю DOM-элемент из какого-то стандартного шаблона и заполняю уникальными данными. Даже там, где повторяются картинки, я использую template: чтобы не было кэширования и тест по памяти был чистым.

69ec2041f3f28a7c301bcb1d031fbd2f.png

Когда я создаю все элементы, я добавляю их в один фрагмент — это тоже оптимизация, которую вы все знаете. Чтобы почистить страницы, я буду просто — шашки наголо! — чистить контейнер.

efc78d274088cff8afe28c0021782f3b.png

Это работает быстрее.

Итак, сборка мусора.

e7eb0d25754f8a1c7a2ee05ca69a07ed.png

Сборка мусора — это такой процесс, который предназначен для оптимизации работы с памятью. Он не контролируется нами. Браузер сам запускает его тогда, когда понимает, что выделенная под эту вкладку память заканчивается и нужно удалить старые неиспользуемые объекты.

Старые неиспользуемые объекты — это объекты, на которые больше нет ссылок. То есть это объекты, не записанные в переменные, в объекты, в массивы — в общем, никуда.

Казалось бы, да, это крутой процесс, нам нужна сборка мусора, потому что память действительно ограниченна и ее нужно освобождать.

Почему это может быть проблемой? Потому что мы не знаем, сколько времени будет занимать сборка мусора, и мы не знаем, когда она произойдет.

Давайте посмотрим на примере.

bb9f5608f0d6b83e617a44d7d5af8fc1.png

Здесь я записал профиль тайм-лайна переключения наших страниц с котиками. На верхнем графике видны всплески производительности процессора — загруженности процессора.

На нижнем графике видно, что сначала память идет вверх — это график использования памяти –, а потом ступень вниз. Это как раз и есть процесс сборки мусора.

Мне хватило памяти на первые две страницы, и я отрисовал 10 000 котиков. Дальше память закончилась и, чтобы отрисовать еще 5 000 котиков, я удалил старых, потому что они больше не используются.

В принципе это круто. Действительно, браузер обо мне позаботился и удалил то, что я не использую. В чем проблема?

3e41319cfa2c8b240fcae1f6ebe94371.png

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

Если сложить 4 записи garbadge collecting«а, видно, что процесс сборки мусора занял 134 мс — это 10 кадров при 60 Fps.

bb3945a2c34f6f45666781c57cb8777c.png

То есть если бы вы хотели проанимировать за какое-то определенное время перемещение блока на 600 Ps, то у вас пропало бы перемещение на 100 Ps просто за счет того, что браузер решил почистить память. Вы не контролируете ни наступление этого процесса, ни длительность. Это плохо.

Утечка памяти — ситуация, когда при сборке мусора некоторые неиспользуемые объекты остаются в памяти, потому что сборщик мусора считает, что они могут использоваться


Вторая проблема абсолютно противоположна первой. Она называется утечка памяти.

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

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

Посмотрим на примере кода.

7490429f0d82159574f7182b4b6cc304.png

Здесь есть то же самое переключение страниц, но каждый раз, когда я вставляю на страницу новые элементы, на каждую фотографию я добавляю обработчик.

Допустим, мне нужно нажать на пробел. Я повесил на всякий случай на документ обработчик, чтобы Keydo никуда не исчез. Что произойдет в этом случае?

Когда я чищу контейнер с помощью удаления HTML, у меня удалятся DOM- ноды с DOM-дерева. Но обработчики на документе останутся, потому что документ останется на странице. Ничего с ним не произойдет. Когда будет происходить сборка мусора, сборщик мусора эти обработчики не удалит.
Давайте посмотрим, что написано внутри этих обработчиков?

Внутри этих обработчиков используется нода. Получается, что и ноды не удалятся из памяти, потому что на них есть ссылка. Это и называется утечка памяти. С точки зрения логики мне ни йоды, ни обработчики не нужны. Но сборщик об этом не знает, потому что с его точки зрения они используются.

Что произойдет в этом случае? Посмотрим на график.

9ee60023089338228090f8e1c3e2cc93.png

Из волн, которые поднимаются и опускаются, график превратился в лестницу, которая растет наверх. С каждым переключением страницы память используется все больше –, но не чистится, поскольку в памяти остаются обработчики и ноды.

Память не освобождается. Компьютер тоже начинает тормозить.

Почему? Потому что большая память — это очень плохо. Процессор будет забиваться при выполнении операций на большом DOM«е.

В итоге получается, что когда мы пытаемся оптимизировать что-либо с помощью памяти, получаем большой риск.

f130349727beab66fb03714c50b85836.png

Сам Брендан Айк, который создал Java-скрипт, в недавнем интервью (WebAssembly) сказал: «Java-скрипт — хороший язык. Он быстрый и по производительности иногда может тягаться с C, но проблема начинается, когда возникает сборка мусора, потому что мы не знаем, когда к нам придет сборщик и сколько времени он проработает».

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


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

c23fcc797905b81581ebc8cf52605a0c.png

Поэтому давайте посмотрим на другую сторону картины, которую я показывал: как можно оптимизировать скорость приложения с точки зрения процессора — основного вычислительного устройства.

646f068dc68b7acbc8cb4e3f5050b3c3.png

Есть три основных способа ускорить работу процессора:

  1. Уменьшить объем вычислений;
  2. Затротлить — чуть позже я поясню, что это такое;
  3. Не пользоваться процессором. Это тоже достаточно странный, но хороший способ оптимизировать процессор.


0ee3def2bc897340f43f863adffa686f.png

Давайте посмотрим на нашу картину с котиками.

6f3f60b62ed8e1a8cf693f5d21e02805.png

Я говорил, что у меня отрисовывается пять страниц по 5 000 котиков. В принципе, это, кстати, реальный объем. Вы можете поскроллить в течение 0,5 минуты этих котиков, и у вас получится DOM на 5 000 элементов.

Но если подумать — для первой загрузки это не нужно. Мы сейчас видим четыре ряда и пять колонок котиков. Это 20 котиков. Получается, что пользователь, открывая страницу в первый раз, видит 20 котиков, а не все 5 000 картинок.

А браузер отрисовывает 5 000. Получается, что мы отрисовываем очень много лишнего. Зачем рисовать 5 000 котиков, если показываем 20?

Хорошо, мы даже можем сделать задел — пользователь может скроллить сайт и ему тоже нужно что-то показывать. Но если отрисовать 100 котиков, это уже будет 5 экранов.

3832172c25e831b51952dea7d0015b4d.png

Поэтому первое, что можно сделать, — уменьшить объем DOM«а. Это самый простой способ. Вы уменьшаете DOM, и все работает быстрее.

Давайте я вам это докажу с точки зрения профайлера.

40b01c93f409417052342600e632ba81.png

Я отрисовываю первую страницу на 5 000 элементов и с помощью профайлера записываю процесс загрузки. Кстати, здесь есть кнопка «Reload», и если вы на нее нажмете, профайлер будет записывать скорость загрузки страницы. Он ее перезагрузит и, когда страница полностью отрисуется и все будет готово, он прекратит запись этого скриншота, и 5 000 котиков отрисуются за четыре секунды.

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

b735a4a2de5edcf187e9596630ec7ce5.png

Если уменьшить страницу до 100 элементов (до пяти экранов), то загрузка будет идти всего 0,5 с. Причем пользователь сразу увидит готовый результат — без оберток.

Поэтому в первую очередь нужно уменьшить объемы вычислений.

Второй момент — это тротлинг.

4b4f9ed27ed0e029fbecd32dc3bf5979.png

Что такое тротлинг?

Представьте, что у вас есть определенная частота кадров и она не вписывается в ту частоту кадров, которую мы выбрали. Мы об этом уже говорили.

Тротлинг — это такой способ мышления, когда мы делаем шаг назад и пытаемся понять, действительно ли нам нужна та частота обновления, которая есть?

c88f5e703cdab4e196230ed3148ab0a9.png

Допустим, это 60 Fps, и операции выполняются с этой частотой. Но вычисления занимают больше 16,5 мс. И тут нужно подумать — действительно ли нужно вписывать эти вычисления в 16,5 мс? Если нет, то мы можем прорядить частоту до нужной.

Давайте рассмотрим пример. Мы только что оптимизировали котиков и показываем не 5 000 котиков, а 100. Давайте изменим способ взаимодействия пользователя с этими котиками. Мы не будем показывать сразу большие страницы, мы будем показывать котиков по мере необходимости.

b3c631daa8118cccb3f13bf362c930e6.png

Для этого нужен динамический скролл. Мы скроллим, доходим до низа, показываем следующую страницу. Здесь код примерно об этом и говорит.

Но я в этот код дописал немного статистики. Я записал дельту в пикселях — как часто у меня срабатывает событие скролла, и счетчик общих событий скролла. Когда я проскроллил страницу сверху вниз, при ее высоте в 1 000 ps событие скролла срабатывало каждые четыре. Сверху донизу произошло 500 проверок.

b74f403fd64b7604dd3699e260f07211.png

Почему так много? Потому что скролл происходит как раз с частотой 60 Fps — той самой. Причем вчера LG показал Ipad с частотой экрана 120 Гц, значит, они теперь будут стараться делать 120 Fps, а не 60. И тут нужно будет еще сильнее задуматься об этом. Помните осла из мультика про Шрека, который спрашивал «Мы уже приехали? Мы уже приехали?» —  точно так же ведет себя моя проверка. Она работает слишком часто и навязчиво.

Тротлинг заключается в том, чтобы прорядить количество кадров. Мне не нужно проверять каждые 4 ps до скроллинга низа страницы. Я могу это делать, допустим, 1 раз в 100 мс.

Здесь я добавил небольшую проверку, основанную на датах.

4ac15819a588706e7ebb3126f7d4a846.png

Я смотрю, сколько времени прошло с прошлой проверки, и запускаю следующую. В чем суть? Событие скролла продолжает происходить, но я использую не все кадры скролла, а только некоторые из них — те, которые попадают под мои условия.

Когда я использовал 100 мс, проверка выполнялась каждые 20–30 ps, и сверху донизу произошло всего лишь 100 проверок. В принципе это нормально — раз в 100 ps спросить, не находимся ли мы внизу.

Это второй способ вычисления. Проверьте частоту кадров. Может быть, для определенной задачи вам не нужно 60 Fps и можно ее снизить.

Третий способ — отдать вычисления.

d9a12d88a827fe23935448d90b93435c.png

Как можно отдать вычисления с процессора? Есть несколько вариантов:

  1. Можно некоторые вычисления отдать на видеокарту.
  2. Некоторые вычисления можно отдать на сервер;
  3. Можно отдать вычисления в другой поток. Это не разгрузит процессор пользователя —, но разгрузит процесс, который открыт во вкладке.


Рассмотрим каждый из этих методов.

e9d95ccf9799b0431149649669f7a6c7.png

Для начала расскажу, почему браузерные игры делаются не на SVG, а на сanvas.

Браузерные игры — это такая штука, в которой есть очень много элементов disposable, которые вы выкидываете. Вы создаете их и выкидываете, создаете и выкидываете. В таких случаях, когда есть сложное взаимодействие и большое количество не долгоиграющих маленьких элементов, есть смысл использовать сanvas.

Рассмотрим сравнение идеологии SVG и сanvas.

e57fd9cd2629e7cee2dd99612efb29e6.png

Под SVG, когда вы что-то отрисовываете, нужен DOM-элемент. SVG — это DOM. Вы же описываете формат в виде разметки, а, как мы раньше уже выяснили, вся разметка попадает в JS в виде DOM-дерева — в виде объекта, например, с класс-листом или со всеми остальными свойствами.

Когда вы пишете на сanvas, вы просто оперируете пикселями. У вас есть методы, которые описывают взаимодействие с сanvas. В итоге получаются пиксели на экране — и больше ничего.
Поэтому на сanvas приходится придумывать свои структуры данных — поскольку нет DOM-дерева, которое за нас было кем-то придумано. Но зато это может быть чуть-чуть лучше для решения задач, чем те структуры, которые предлагает SVG.

Раз у SVG есть какая-то стандартная структура —  у нее есть API. То есть с SVG можно делать какие-то взаимодействия, например, обновлять по одиночке, анимировать.

На сanvas всего этого делать нельзя. Вам придется все писать руками, как на ассемблере. Но зато вы можете получить прирост производительности. Ведь SVG — это DOM, и он будет считаться на процессоре, а отрисовка пикселей — на видеокарте.

Давайте рассмотрим пример, для чего немного повращаем Землю.

b6198ba8ea1c1da47a2e0aa1023e7118.png

Здесь разрешение смешное по нынешним временам — 800×600 — вообще ни о чем. Та Земля, которую вы видите, — это векторная графика. Все страны описаны через один сложный path. Я не отрисовывал каждую страну отдельно в виде объекта. Они все уложены в одну линию определенной формы.

Я буду обновлять кадры через requestAnimationFrame. То есть браузер сам мне скажет, какой у меня Fps для отрисовки этой штуки. Он сам поймет, за сколько он сможет отрисовать 1 кадр.

В качестве анимации я буду проворачивать Землю на 360° — от Лондона до Лондона. Так было проще написать — в массив мне нужно передать 0, потому что это координата Лондона.

7f4c5f0130632faf909160bbd0eb1c47.png

Здесь я записал профиль работы сanvas. Во-первых, посмотрите на нижний график. Здесь используется только GPU. То есть только видеокарта для анимации. Выше ничего нет.

Здесь задействована вкладка Main. Это работа непосредственно процессора по изменению в массиве числа 0 на число 360 и просчет этого контура в зависимости от угла — умножает известные ему координаты стран на формулу проекции их на окружность.

Дальше используется только видеокарта.

В итоге я запустил несколько тестов, и у меня получилось, что в среднем анимация длится шесть секунд. С помощью нехитрых вычислений — 360° за 60 с — получается 60 Fps — все хорошо.

b5efe91fb7f77468b848b9caaf1350c1.png

А вот SVG справился чуть хуже. Почему? Потому что если посмотреть на две последние линии, мы увидим заполненную вкладку Raster. Это значит, что SVG создавал под каждый кадр DOM-объект, просчитывал все его параметры полностью с помощью процессора, а не с помощью GPU, и с помощью видеокарты уже рендерил его в виде пикселей.

То есть сначала DOM-объект, который долго и сложно просчитывается, потом пиксели на экране. Это достаточно серьезно просадило производительность. Уходило примерно восемь секунд, а это около 45 Fps.

Я обвел 45 Fps красной рамочкой, потому что это, что называется, за гранью позора. Вы скажете: «Ты что говоришь? Есть даже фильмы, которые идут с частотой 24 Fps».

Неправда. Даже фильмы, которые еще на заре кинематографа записывались с частотой 24 Fps, показывали с частотой 48 Fps. Эту концепцию нам объясняет Томас Эдисон: «Да, человек будет видеть как движение 24 Fps, но он будет видеть мерцание от обновления картинки».

0529e0a25d89d617ec435185f2253937.png

То есть он будет видеть движущуюся картинку, при этом замечать мерцание. Чтобы этого не происходило, нужно как минимум 48 Fps.

Для этого в старых фильмах каждый кадр показывался два раза.

То есть даже на заре кинематографа фильмы показывались с частотой 48 Fps. А SVG не справился, провалил тест производительности, и это плохо.

То есть получается, что в определенных случаях сanvas лучше SVG. Например, если у вас много таких disposable элементов, которые нужно выкидывать, лучше использовать сanvas.

Еще один тест.

a30bec473e4f2c0682512af5a0a11cd4.png

Когда я готовился и прогонял все примеры кода, случайно забыл одну строчку — очистку предыдущего кадра. У меня получилась такая странная картинка. Я решил не просто избавиться от этого бага, а посмотреть, куда меня приведет моя ошибка, и замерил производительность этой штуки.

cc999b6eecd12bae9a00337513c79795.png

Когда я замерил производительность оборота Земли на сanvas без чистки сanvas, у меня получилось 60 Fps. Я пять раз перепроверил, не ошибся ли я картинкой. 60 Fps — и наплевать на сanvas.

Как вы думаете, как справился SVG?

ab8854564af56ed689652ac8b4b69657.png

Полная анимация заняла 24 минуты! У него ничего не чистилось, и произошла классическая утечка. Память росла и росла. Я хотел показать профайлер, но в первый раз увидел окно смерти на профайлере. Это вообще очень странно.

Чтобы объяснить, что произошло, я покажу профайлер на обычном SVG.

d6dbec10f18035f7d6b144aa436a8864.png

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

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

Нет, лучше canvas.

f9dae4800444278845a44de5fa79f325.png

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

d8140e3b2d43d883db3ddb65bcf258e3.png

Я подумал: графика? Графика. SVG не подойдет, потому что шаг обновления — 1ps, то есть каждый пиксель у меня что-то значит. Поэтому должен быть сanvas.

9b50d026e2004b960ab3d95260ccbc7d.png

Я решил эту задачу на сanvas. Приходила структура данных, я проходил по ней, ставил точку на карте, которая имела цвет определенной насыщенности.

Что получилось? На самом деле мне не сильно понравилось решение, потому что:

  1. Мне пришлось писать очень много костылей. Canvas — это практически графический ассемблер, у него API достаточно низкого уровня и приходилось вручную работать с каждым пикселем.
  2. Мне приходилось запускать очень много вычислений, а пользователь видел одно и то же. То есть когда пользователь чуть сдвигал карту — я пересчитывал все заново. df0e9fa2ef71e33e3cfc99f1994d6eec.png


Тогда я подумал: «Хорошо, я умный разработчик, я решил офигенно сложную задачу — нарисовал теплокарту, но стоит подумать о пользователе и не выделываться, а просто:

  • прийти к бэкендеру,
  • у которого Python,
  • красивая библи

    © Habrahabr.ru