Русский
Русский
English
Статистика
Реклама

Client-side performance

Оптимизация производительности фронтенда. Часть 1. Critical Render Path

05.08.2020 16:23:28 | Автор: admin

Здравствуйте. Меня зовут Ник, я фронтенд разработчик (жидкие аплодисменты). Кроме того, что я пишу код, я преподаю в Школе программистов hh.ru.


Записи наших лекций от 2018-2019 учебного года можно посмотреть на youtube


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



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


Лонгридом можно пользоваться как справочником, чтобы не читать за один присест. Вот список тем, которые мы затронем:


  1. Зачем думать о производительности
  2. FMP, TTI + подробнее в докладе
  3. Critical render path, DOM, CSSOM, RenderTree
  4. Шаги по улучшению производительности первой загрузки + подробнее в докладе

Для удобства восприятия я решил разделить статью на две части. Вторая часть будет о таких операциях, как layout, repaint, composite и их оптимизации.


Зачем вообще думать о производительности? Мотивационная часть


0.1 секунда это тот gap, который позволяет пользователю осознать, что именно его клик мышки, удар по клавиатуре побудил эти изменения в приложении\интерфейсе.
Кажется, у всех было то неловкое чувство, когда ты сочиняешь письмо\код\любой другой текст, а интерфейс "за тобой не успевает". Ты уже пишешь второе слово, а на экране всё еще песочные часы (если мы про windows) и еле-еле набирается первое. Аналогично и с кликами на кнопки. Я хочу, чтобы интерфейс мне подсказывал, мол, "окей, я тебя услышал, ща все будет".
За примером далеко ходить не нужно. Я пользуюсь веб-версией одного российского почтовика (не будем называть имен) и когда выделяю письма для их удаления, то бывают большие задержки. И я не понимаю: то ли я не попал по кнопке", то ли сайт тормознутый. И обычно верно второе.
Почему 0.1 секунда? Дело в том, что мы замечаем и успеваем обработать даже куда более ограниченные по времени изменения и наш мозг находится "в контексте".
В качестве яркого примера посмотрите клип 30 seconds to mars hurricane. Там есть вставки на кадр-два с текстом не 9:30. Глаз успевает не только осознать вставку, но и частично определить контент.


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


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


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


10 секунд если погуглить, на всякую аналитику вроде "среднее время пользователя на сайте" мы увидем число: 30 секунд. Сайт загружающийся 5 секунд убивает 1/6 времени пользователя. 10 секунд треть.


Дальше идут два числа 1 минута и 10 минут. первое идеальное время для того, чтобы пользователь выполнил небольшую задачу на сайте прочитал описание товара, зарегистрировался на сайте и т.д. Почему минута? В наши дни мы не так много тратим время на концентрацию на одной вещи. Как правило наше внимание очень быстро перескакивает с одного на другое. Открыл статью, прочитал десятую часть, дальше коллега мем в телегу отправил, тут триггер зажегся, новости про короновирус, верните мне мой 2007, вот это все. В общем к статье получится вернуться только через час.


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


Большие компании даже хорошую аналитику для таких целей имеют:


  • Walmart: 1 секунда ускорения + 2% конверсии
  • Amazon: 0,1 секунды увеличивает выручку на 1%
  • Что-то аналогичное есть у Яндекса (киньте в комментариях ссылку, я потерял)

И последний мотивационный пост в статье от википедии:



Однако достаточно вводной, пора двигаться дальше.


Два извечных вопроса


Давайте запустим lighthouse на hh.ru. Выглядит всё очень не очень (запуск на mobile, на desktop все сильно лучше):



Появляется два традиционных вопроса:


  1. Кто в этом виноват?
  2. Что с этим делать?

Хотя первый вопрос я бы заменил на "Как это расшифровать".
Сразу спойлер: картинки "как круто стало в конце не будет. Как сделать лучше мы знаем. Но есть определенные ограничения.


Давайте разбираться


В первом приближении у нас есть 3 потенциальных сценария:


  1. Отрисовка страницы (с html от сервера)
  2. Работа загруженной страницы (клики пользователя и т.д.)
  3. SPA переходы между страницами без перезагрузки

У пользователя существует два этапа при отрисовке страницы, когда он может частично взаимодействовать с сайтом. И полноценно: FMP (First Meaningful Paint) и TTI (Time to interactive), когда ресурсы загружены, а скрипты проинициализированы:



Если судить о значении для пользователя: FMP == текст, есть верстка и пользователь может начать потреблять контент (конечно, если вы не инстаграмм). TTI == сайт готов к работе. Скрипты загружены, проинициализированны, ресурсы загружены.


Подробнее о расчетах FMP\TTI и как мониторить эту информацию на своем сайте, снимая данные с реальных людей, я рассказывал на РИТ++. В этом же докладе я говорил и про эксперименты, которые мы проводили по ускорению и замедлению скорости работы сайта.


Наиболее важный показатель для нас FMP. Соискатели открывают поиск, затем накликивают "открыть в новой вкладке" большое количество вакансий, а затем читают и принимают решение об отклике. C некоторыми оговорками (нюансы рендера браузера) FMP можно воспринимать как одну из основных метрик, которая описывает Critical render path. Critical render path это набор действий и ресурсов, которые браузер должен совершить, загрузить и обработать, чтобы пользователь получил свой первый результат, пригодный для работы. То есть это минимальный набор html, css и блокирующие скрипты (если так еще кто-то делает), без которых сайт не отобразится пользователю.


Critical render path или что браузер делает для того, чтобы пользователь увидел тот самый текст?


TL&DR;


  1. Сделать запрос (DNS resolve, TCP поход и т.п.);
  2. Получить HTML-документ;
  3. Провести парсинг HTML на предмет включенных ресурсов;
  4. Построить DOM tree (document object model);
  5. Отправить запросы критических ресурсов. CSS, блокирующий JS (параллельно с предыдущим пунктом);
  6. Получить весь CSS-код (также запускаем запросы на JS-файлы);
  7. Построить CSSOM tree;
  8. Выполнить весь полученный JS-код. Здесь могут вызываться layout, если из js кода происходит форсирование reflow;
  9. Перестроить DOM tree (при необходимости);
  10. Построить Render tree;
  11. Отрисовать страницу (layout paint Composite).

Теперь пройдемся по пунктам отдельно:


Подробно:


Request



Формируем запрос, резолвим DNS, IP, TCP поход, передача запроса и т.п. Байтики бегают по сокетам, сервер получил запрос.


Response


Бекенды зашумели вентиляторами, обработали запрос, записали обратно данные в сокет и нам пришел ответ. Например, вот такой:



Мы получили байты, сформировали из нее строку, основываясь на данных text/html, и после пометки нашего запрос как "navigate" (кому интересно это можно посмотреть в ServiceWorker) браузер должен сформировать из этого html DOM.


Сказано, сделано:


Обработка DOM


DOM


Мы получаем строку или поток данных. На этом этапе браузер парсит и превращает полученную строку в объект:



Это только каркас. Сейчас браузер ничего не знает про стили и про то, каким образом всё это рендерить.


Загрузка блокирующих ресурсов


Браузер будет последовательно обрабатывать полученный html и каждый ресурс. CSS, JS может быть загружен как синхронно, блокируя дальнейшую обработку DOM, так и асинхронно (для css это способ с preload + сменой rel после загрузки на stylesheet). Поэтому каждый раз, когда браузер будет встречать блокирующую загрузку стилей или JS, он будет формировать запрос за этим ресурсом.
Для каждого такого ресурса повторяем путь с запросом, ответом, парсингом ответа. Здесь появляются ограничения, например, количество одновременных запросов на домен. Предположим, что все блокирующие запросы были описаны внутри тега head, браузер сделал запросы, получил нужные для рендера стили. После формирования DOM переходим к следующему этапу:


CSSOM


Предположим, что помимо meta и title был тег style (либо link). На данном этапе браузер берет DOM, берет CSS, собирает соответствия, и на выходе мы получаем объектную модель для CSS. Выглядит это примерно так:



Левая часть (head) для CSSOM практически не интересна, она не отображается пользователю. А для остальных узлов мы определили стили, которые браузеру нужно применить.


CSSOM важен, так как его понимание позволяет браузеру сформировать RenderTree.


RenderTree


Последний шаг на этапе от формирования деревьев к рендеру.


На этом этапе мы формируем дерево, которое будет рендериться. В нашем примере левая часть дерева, которая включает head, рендериться не будет. Так что эту часть можно опустить:



То есть, именно это дерево мы и отрендерим. Почему его? Если мы зайдем в DevTools там отображается DOM". Дело в том, что, хоть в DevTools и присутствуют все DOM элементы, все расчеты и вычисленные свойства уже основаны на RenderTree.


Проверить крайне легко:



Здесь мы выделили кнопку во вкладке Elements. Мы получили всю "вычисленную" информацию. Её размеры, положение, стили, наследование стилей и т.д.
Когда мы получили RenderTree, наша следующая задача выполнить Layout Paint Composite нашего приложения. После этих трех этапов пользователь и увидит наш сайт.
Layout Paint Composite могут быть болью не только во время первого рендера, но и при работе пользователя с сайтом. Поэтому мы разберем их во второй части статьи.


Что можно сделать, чтобы улучшить метрики FMP и TTI?


TL&DR;


1) Работа с ресурсами:


1.1) Разнести блокирующие ресурсы по страницам. Как js, так и css. Хранить реиспользуемый между страницами код либо в отдельных бандлах, либо в отдельных небольших модулях.


1.2) Грузить то, что пользователю нужно в начале работы со страницей (очень спорный момент!)


1.3) Вынести third-party скрипты


1.4) Грузить картинки лениво


2) HTTP2.0 / HTTP3.0:


2.1) мультиплексинг


2.2) сжатие заголовков


2.3) Server push


3) Brotli


4) Кэш, ETag + Service worker


Подробно:


Работа с ресурсами


Разносим блокирующие ресурсы. JS


Основной болью являются 2 вещи: блокирующие ресурсы и размер этих ресурсов.
Самый главный совет для больших сайтов это разнести блокирующие стили, ресурсы по страницам. Реиспользумый код выносить в отдельные бандлы или модули. Для этого можно воспользоваться условным loadable-components или react-imported-component для реакта и похожими решениями для vue и т.д. Если наши компоненты импортируют стили, то мы сможем также и разбить стили на отдельные страницы.
На выходе мы получаем:


  1. бандлы с реиспользуемыми JS модулями
  2. страничные бандлы.

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


Изначальная расстановка:



Стратегия 1: делаем зависимость модуль страницы, которые используют модуль



Так, чтобы загрузить главную страницу (index.html), нам нужно будет загрузить 2 бандла: Common.JS + applicant+index.JS, а для страницы /applicant нужно загрузить все 4 бандла. На больших сайтах таких модулей может быть очень много. В этом случае нам помогает решить эту проблему использование HTTP2.0.


Итого:
+: Данные распределены между страницами, мы грузим всегда только необходимое
+: Модули легко кэшируются, после релиза не обязательно обновлять все бандлы у пользователя
-: Много сетевых издержек для получения отдельных ресурсов. Фиксим с помощью мультиплексирования HTTP2.0.


Стратегия 2: реиспользуемые модули хранятся отдельно:



В данном случае каждый файл, который используется больше, чем на одной странице, мы отправляем отдельно. По сравнению с первой стратегией, на средних и больших сайтах мы получаем трагический рост отдельных небольших файлов. Многие из которых будут меньше 1 Кб? что не кэшируется тем же Chrome.


+: Во время релизов большая часть модулей останется в кэше у пользователя
-: Еще больший рост сетевых издержек, если у пользователя нет HTTP2.0
-: Кэши могут не работать, так как файлы будут меньше 1 Кб. Здесь нас может спасти Service worker. О нем будет ниже.


Эта стратегия имеет право на жизнь, так как минусы решаемы.


Стратегия 3: Иметь большой бандл реиспользуемого кода:



+: малое количество бандлов. Для загрузки страницы нужен страничный JS + Common.JS
-: При первой загрузке мы получаем очень много unused JS
-: При релизе скорее всего пользователю придется скачивать общий бандл заново, преимущество кэшей теряется.


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


Анти-Стратегия 1: Каждая страница хранит весь список зависимостей, выносим только common:



В данном случае мы получаем большой оверхед. При переходе с одного ресурса на другой пользователь закачивает себе модули, которые уже у него были несколько раз. Например, пользователь заходит на главную и скачивает 2 бандла: Common.JS и Index.JS затем авторизуется и попадает на страницу соискателя. Итого, код для Dropdown.JS и Graph.JS будет скачан дважды.


Пожалуйста, не делайте так :)


Итого


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


Оффтоп. Почему 30 Кб JS это больнее, чем 30 Кб картинки


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


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


Итого, на обработку JS кода мы тратим больше времени, чем на ту же картинку.


Разносим блокирующие ресурсы. CSS


Данное улучшение напрямую влияет на FMP, если у вас не асинхронный CSS.
Если вы используете react\vue\angular, то для стилей стоит сделать то же, что и в предыдущем пункте. Как правило, в том же react-коде мы имеем импорты вида:


import './styles.css'

Это значит, что во время бандлинга JS-кода мы можем разделить и CSS, воспользовавшись одной из стратегий выше. Это поможет вебпаку или другому бандлеру получить аналогичные common.css, applicant-page.css и applicant+employer.css.
Если разбить используемый CSS не получается, можно посмотреть в сторону used-styles и статью на эту тему: "optimising css delivery". kashey спасибо за клевые инструменты :)


Это поможет ускорить загрузку, например в случае с hh.ru почти на секунду по эстимейтам lighthouse:



Грузим то, что увидит пользователь, а не всю страницу.


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


Идея данной оптимизации в том, чтобы управлять загрузкой ваших ресурсов. В начале загрузить блокирующим способом тот CSS, который жизненно необходим для открытия страницы. Весь CSS, который относится к всплывающим элементам, popup-ам, которые спрятаны под JS кодом можно будет грузить асинхронно, например, добавив rel=stylesheet уже из JS кода, либо воспользовавшись prefetch с onload колбеком.


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


Выносим third-party скрипты


У нас в hh их много. Очень много!
Например? здесь в ТОП-10 самых тяжелых скриптов 7 third-party.



Что мы можем с этим сделать?


  1. Убедиться, что все ресурсы грузятся асинхронно и не влияют на FMP.
  2. Для рекламы и прочих вещей (излишней аналитики, popup-ов поддержки) снизить их влияние на основной "полезный" код. Это можно сделать, начав их инициализировать из нашего кода по requestIdleCallback. Эта функция запланирует вызов колбека с низшим приоритетом, когда наш браузер будет простаивать.

Такой подход позволяет нам экономить на FMP, хотя в TTI мы по-прежнему будем наблюдать проблемы. Это также поможет нам отложить работу прожорливых third-part скриптов.


Грузим картинки лениво


Картинки влияют на ваш TTI. Если вы понимаете, что пользователи из-за лишней работы браузера могут страдать, попробуйте начать грузить картинки, которые не попадают на экран, лениво. То есть:


  1. Картинки попадающие на первый экран, грузим как обычно
  2. Оставшимся картинкам проставляем специальные атрибуты, чтобы изображение грузилось только по мере того, как пользователь проскроллит к нему
  3. Используем любую библиотеку\велосипед для ленивой загрузки картинок. Вот здесь достаточно хорошо описан подход.

HTTP2.0


Многие из пунктов HTTP2.0 могут не сэкономить заметного количества времени, но разобрать их стоит.


HTTP2.0 Мультиплексинг


В случае если сайт загружает большое количество ресурсов, HTTP2.0 с мультиплексингом может сильно помочь.
Предположим, у нас есть 6 блокирующих ресурсов на домене, которые нужно скачать, чтобы отобразить сайт. Это могут быть стили или блокирующий JS. Считаем, что пользователь уже загрузил HTML:



Для отображения контента браузер будет производить параллельные запросы к каждому ресурсу:



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


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


Именно поэтому совет нужен бандлинг и отдавать пользователю один бандл с нужным ему на этой странице CSS \ JS картинками работает.


Что такое мультиплексирование?


Мультиплексирование позволяет нам грузить ресурсы внутри одного HTTP запроса:



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


HTTP2.0 Сжатие заголовков


До http2.0 сжатия заголовков не существовало. В HTTP2.0 был анонсирован HPACK, которые отвечает за сжатие. Здесь можно почитать подробнее.


Иногда заголовки могут занимать значительный объем запроса. О том, как работает сжатие заголовков HPACK вкратце:


Используется два словаря:


  1. Статический для базовых заголовков
  2. Динамический для кастомных

Для префиксного кодирования используется Huffman coding. На практике эта экономия выходит не сильно высокой и малозаметной.


HTTP2.0 Server push


В базовом варианте server push реализовать несложно. Проще всего сделать такую штуку для статических сайтов. Идея простая: вместе с запросом html страницы, наш веб-сервер заранее знает, что пользователю нужно отдать такой-то css, такую-то картинку и такой-то JS.


Реализуется достаточно просто (nginx):


location = /index.html {    http2_push /style.css;    http2_push /bundle.JS;    http2_push /image.jpg;  }

Проверить, что все работает несложно:



Такой способ будет работать для небольших статических сайтов. Если у вас большой сайт, то придется настраивать сложный пайп-лайн. Когда после сборки имена ваших ресурсов записываются в какой-то словарь, который затем собирает конфиг для nginx, содержащий урлы и прописывает http2_push правила.


Сжатие данных


Наиболее популярными решениями сейчас являются gzip и brotli. Вот сайт, где есть хорошее сравнение степени сжатия двух алгоритмов:



Примерно полтора года назад мы в hh.ru перешли с gzip на бротли. Размер нашего основного бандла уменьшился с 736 КБ до 657. Выиграли почти 12%.


Главным недостатком Brotli можно назвать затраты на "упаковку" данных. В среднем она тяжелее, чем gzip. Поэтому на nginx можно указать правило, чтобы он паковал ресурс и складывал его рядом, дабы не делать сжатие на каждый запрос. Ну или при сборке сразу класть сжатый вариант. Аналогично можно делать и для gzip.


Но в любом случае Brotli ваш бро! На сайтах среднего размера он позволяет сэкономить секунду-полторы загрузки на слабых 3G сетях, что сильно улучшает как пользовательские ощущения, так и метрики в том же lighthouse.


КЭШ


Примечание: описанный здесь способ не поможет вам получить дополнительные баллы у lighthouse, но поможет при работе с реальными пользователями. Этот способ положительно сказывается как на FMP, так и на TTI.
Сам кэш можно включить либо с помощью заголовков у статики, либо с помощью Service Worker.
Если мы говорим о заголовках, для этого служит 3 основных параметра:


  1. last-modified или expires
  2. ETag
  3. Cache-control

Первые два (last-modified и expires) работают по дате, второй ETag это ключ, который используется при запросе и если ключи совпадают, сервер ответит 304 кодом. Если не совпадут, то сервер отправит нужный ресурс. Включается на Nginx очень просто:


location ~* ^.+\.(js|css)$ {            ...    etag on;}

Disk cache проверяется очень просто через dev tools:



Cache-control это стратегия того, как мы будем кэшировать информацию. Мы можем или отключить его вовсе, установив cache-control: no-cache, что может быть полезным для html запросов, которые часто меняются. Либо мы можем указать очень большой max-age, чтобы данные хранились как можно дольше. Для нашей статики, мы устанавливаем вот такой Cache-control:


cache-control: max-age=315360000, public

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



У нас есть "три способа" запуска нашего приложения: cold\warm и hot run. Идеально, если мы будем запускать приложение в режиме hot run, потому что на этом этапе мы не тратим время на компиляцию нашего года. Его достаточно только десериализовать.


Для того, чтобы получить hot run, пользователю нужно зайти на сайт в третий раз (за одними и теми же ресурсами) в таймслоте в 72 часа. В третий потому что во второй раз будет выполнен warm run, который будет компилировать и сериализовать данные в дисковый кэш.


Мы на самом деле можем зафорсить hot run, использовав Service Worker. Механизм следующий:


  1. Устанавливаем пользователю Service Worker;
  2. Service worker подписывается на fetch;
  3. Если происходит fetch за статикой, то сохраняем результат в кэш;
  4. Добавляем перед отправкой запроса проверку на наличие ресурса в кэше.

Этот подход позволяет нам забустить дисковый кэш и использовать hot run уже со второй загрузки сайта. Дополнительное улучшение перфоманса мы получаем для мобильных устройств, которые чаще сбрасывают свой обычный кэш.


Минимальный вариант кода для данного случая:


self.addEventListener('fetch', function(event) {        // Кешируем статику, но не картинки    if (event.request.url.indexOf(staticHost) !== -1 && event.request.url.search(/\.(svg|png|jpeg|jpg|gif)/) === -1) {        return event.respondWith(                        // проверяем наличие данных в кеше            caches.match(event.request).then(function(response) {                if (response) {                    return response;                }                                // Если данных в кеше нет, делаем запрос и сохраняем данные в кеш, который наызываем cacheStatic                return fetch(event.request).then(function(response) {                    caches.open(cacheStatic).then(function(cache) {                        cache.add(event.request.url);                    });                    return response;                });            })        );    }});

Итого


Мы рассмотрели наш Critical render path с точки зрения клиента (не углубляясь в такие вещи, как резолв DNS, handshake, запросы в БД и т.п.). Определили те шаги, которые делает браузер, чтобы сформировать первую страницу для пользователя. Поверхностно посмотрели на сложные способы оптимизации (разделения контента и т.д.)/ И разобрали более простые способы оптимизации: бандлинг, кэш, сжатие, транспорт.


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


Небольшое уточнение: здесь рассмотрены не все способы оптимизации, их заметно больше и появляются новые. Например, в chrome 85 появится свойство content-visibility, которое позволит сократить время на рендеринг за счет отложенных вычислений.

Подробнее..

В диких условиях. Итоги проектов Школы программистов в эпоху самоизоляции

12.08.2020 10:04:09 | Автор: admin
За четыре месяца занятий были прочитаны 54 лекции на двух потоках бекэнд и фронтенд, проведены несколько крутых практикумов с live-codingом. Проверены сотни заданий, на все вопросы получены две сотни ответов. Тут пришел 2020 год и сразу после того как мы сняли с елок гирлянды, всем нам самим пришлось нарядиться в маски и надеть перчатки. А теперь по порядку:



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

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

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

В этот же момент наше обучение перешло в фазу проектов, а мир IT, который мы знаем, переобулся на ходу и перешёл в режим тотальной удаленки. На Школе это тоже отразилось: нам срочно пришлось пересмотреть планы, все наши проекты и процессы обучения перевести в онлайн. И тут хочется поблагодарить наших организаторов и менторов за то, что смогли в экстренном порядке перевести всё обучение в удаленный режим, школьников, которые дошли до этого этапа, смогли сориентироваться и эффективно учиться даже из дома, и всех тех, кто оказывал нам помощь и поддержку.

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

Всего набралось 5 команд, каждая из которых на протяжении трех месяцев работала не покладая рук, глаз и мозга: планируя и разрабатывая, участвуя в ревью кода и ретроспективах. Итогом стали 5 проектов, о которых не стыдно рассказать и которыми хочется похвастаться, например, при устройстве на работу.

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

Вот эти темы:

  • Сервис по формированию коммерческих предложений для работодателей сервис поддержки наших salesов, который позволит работать эффективнее, а нашим клиентам получать действительно индивидуальные предложения;
  • Внутренний сервис для разработчиков, позволяющий геймифицировать процесс написания кода и создавать рейтинги разработчиков по различным критериям приложение должно общаться с нашим GitHub-аккаунтом и показывать данные о тех разработчиках, которые работают быстрее, выше и сильнее;
  • Сервис для оценки качества поисковой выдачи. Наверняка вы слышали /что в интернете кто-то не прав:)/, как кто-то жалуется на то, что в результатах поиска в интернете нашлась какая-то нерелевантная информация. Так вот, у нас на сайте hh.ru тоже так бывает. Чтобы это исправить, нужен сервис, который будет позволять оценивать, насколько поиск был успешным и насколько результаты соответствуют запросу;
  • Внутренний сервис для тимлидов и разработчиков по оценке навыков мы, как и многие технологические компании, поощряем развитие сотрудников и для рекомендаций и помощи тимлидам используем систему оценки навыков. Её MVP был реализован через google forms, но функциональности очень не хватало, поэтому решили сделать свою кастомную систему;
  • Сервис для тегирования вакансий. Сейчас в нашем приложении для вакансии и резюме можно указать ключевые навыки, которые являются приоритетными метками для поиска и сравнения. Их нужно проставлять вручную и не всегда это делают правильно. Цель проекта автоматически вычислять теги на основании других полей вакансии.

Согласитесь, отличные проекты? Тянет на отдельное веб-приложение или полноценную новую фичу уже существующего приложения. Кстати, если бы у вас была возможность предложить какую-то тему или идею для проекта в Школе программистов, то что бы это могло быть?

А пока вы думаете, пара слов о том, какие технологии были использованы в проектах, и какими получились итоги работы команд над проектами:

Сервис опроса компетенций тимлида


Это полноценное веб-приложение, которое работает независимо от нашего основного hh.ru.

На фронтенде использовались:

  • react
  • react final form
  • redux
  • material-ui-kit для ускорения прототипирования интерфейса

На бекенде:


Все части приложения завернуты в Docker.

Команда проекта выделила следующие пункты как итоги своей работы: разработчики создали сервис с нуля, реализовав основу для дальнейшего развития корпоративной системы оценки навыков. Набрались опыта в работе по agile-методологиям и код-ревью. Создали сплоченную команду и хорошо повеселились.

Сервис рейтинга разработчиков


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

На фронтенде использовались:

  • react
  • redux
  • final-form
  • date-fns
  • less как препроцессор для стилей

На бекенде:

  • nuts-and-bolts (NaB)
  • jersey
  • hibernate
  • PostgreSQL

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

По итогу работы команда представила MVP своего проекта, предложила несколько вариантов развития функциональности.

Сервис по формированию коммерческих предложений для работодателей


Это приложение реализовывалось как отдельный сервис внутри нашей экосистемы микросервисов, относящихся к hh.ru

На фронтенде использовались:

  • react
  • react final form
  • redux
  • material-ui-kit для ускорения прототипирования интерфейса

На бекенде:

  • nuts-and-bolts (NaB)
  • jersey
  • hibernate
  • kafka как технология для передачи событий от систем бизнес-аналитики и веб-приложения к новому сервису
  • PostgreSQL

Одним из основных вызовов для этой команды стала необходимость также поднять у себя копию базы MSSQL Server, так как именно там хранятся данные бизнес-процессов, аналитики и клиентов. Отдельную анонимизированную копию с данными использовали в дальнейшем для демонстрации и тестирования.

Сервис для улучшения качества поисковой выдачи


На фронтенде использовались:

  • react
  • redux
  • less, как препроцессор для стилей

На бекенде:

  • nuts-and-bolts (NaB)
  • jersey
  • hibernate
  • PostgreSQL

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

Лучшей же оценкой самого проекта стало пожелание продакт-менеджера поиска поскорей выкатывать сервис в продакшен.

Сервис для тегирования вакансий


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

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

  • react
  • redux
  • less

На бекенде для сбора и анализа данных:

  • nuts-and-bolts (NaB)
  • jersey
  • hibernate
  • PostgreSQL
  • Apache Lucene
  • Яндекс.Танк для нагрузочного тестирования

Основным челленджем стало погружение в ML, изучение метрик TF-IDF, PMI и их производных. На финальном демо команда поделилась тем, что основными трудностями при реализации алгоритма стали: отсутствие достаточного времени для анализа данных, отсутствие метрик качества, чтобы сравнивать алгоритмы и очень большая вариативность в параметрах модели.

Мы уверены, что идеи и их реализации получились достойными нашей юбилейной Школы. А 22 выпускника станут отличными программистами да чего скромничать, они уже стали. 11 из них мы позвали работать к нам, а остальным предоставим рекомендации и приложили усилия, чтобы ребята попали в хорошую компанию!

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

Кстати, набор в одиннадцатую Школу Программистов в самом разгаре. Более того, в этом году мы полностью переходим в онлайн, и в связи с этим мы увеличили количество мест до 40, а заявки теперь можно подавать из любого города нашей необъятной! Если этот опыт окажется успешным, то мы приложим все усилия, чтобы Школа осталась online навсегда.

Обучение полностью бесплатное.

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

1. заполни анкету на сайте

2. выполни тестовое задание

3. пройди online-собеседование с нашими сотрудниками

Мы ждем тебя в нашей Школе!


Хорошего вам дня и вспоминая Мольера, подведем итог: Как приятно знать, что ты что-то узнал!
Подробнее..

Оптимизация производительности фронтенда. Часть 2. Event loop, layout, paint, composite

03.09.2020 14:17:53 | Автор: admin

Ночь. Стук в дверь. Открыть. Стоят двое. "Верите ли вы в Event loop, нашу главную браузерную цепочку?" Вздохнуть. Закрыть дверь. Лечь досыпать. До начала рабочего дня еще 4 часа. А там уже ивент лупы, лейауты и прочая радость


В первой части мы говорили о первой загрузке и работе с ресурсами. Сегодня я расскажу о второй части оптимизации производительности фронтенда. О том, что происходит с нашей страницей, когда она загружена, на что уходит процессорное время и что с этим делать. Ключевые слова: event loop, paint \ repaint, layout \ reflow, composite.



Завязка. Вопросы для самопроверки


Если хочется сразу начать поглощать контент статьи, пропустите этот раздел


Event loop популярен не только в ежедневной работе, но и на собеседованиях. И пойди разберись, где он популярнее. Существует три вопроса, которые можно задать себе, чтобы проверить, понимаете ли вы этот браузерный механизм. Два из них завсегдатаи собеседований, а вот третий показывает понимание event loop для своей практики. Они довольно простые, но будут расставлены по возрастанию сложности:


  1. Будет ли выведено "1" в консоль? Почему?


    function loop() {Promise.resolve().then(loop);   }setTimeout(() => {console.log(1)}, 0);loop();
    

  2. Есть сайт, а на сайте ссылка, у которой при наведении cursor: pointer ставится через :hover стиль CSS и кнопка, у которой также по :hover меняется background-color c серого на синий. Добавляем скрипт:


    while (true);
    

    Вопрос: Что будет если навести мышку на ссылку? А на кнопку? Почему?


  3. Как анимировать выпадающий элемент по height с 0 до auto? Здесь важно обсудить способы c помощью JS и/или CSS. Кстати, если гуглить этот вопрос, то stackoverflow вначале предлагает неверный ответ. Суть понимания event loop и работы браузеров сводится к тому, как замерить то самое height = auto с помощью JS.



Наша цель


Прийти к достаточно глубокому пониманию вот этой схемы:


Идти к ней будем постепенно, с подробными остановками на каждом из этапах


Event Loop


В старых операционных системах была похожая штука. Выглядела она условно вот так:


while (true) {  if (execQueue.isNotEmpty()) {     execQueue.pop().exec();    }}

Такой код всегда забивал ЦПУ процессоров в 100%. Что и было в старых версиях windows. Сейчас планировщики операционных систем очень сложные. Там есть и приоритизация, и исполнение, и различные очереди.


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



Теперь нам нужно откуда-то эти задачи получать.
Зададим себе вопрос: что является триггером, чтобы наш JS код начал выполняться?
Это может быть:


  1. Браузер загрузил тег <script>
  2. Отложенные задачи: setTimeout, setInterval, requestIdleCallback
  3. Ответ от сервера через XmlHttpRequest, fetch и т.п.
  4. События и вызовы подписчиков через браузерное API: click, mousedown, input, blur, visibilitychange, message и так далее, множество их. Часть из них вызывается пользователем (кликнул на кнопку, альт-табнулся и т.д.).
  5. Изменение состояния промисов. В некоторых случаях, это происходит за пределами нашего JS кода
  6. Обзерверы, такие как: DOMMutationObserver, IntersectionObserver
  7. RequestAnimationFrame
  8. Что-то еще? :)

Почти все эти вызовы планируются через WebAPI (его еще иногда называют браузерным API). То есть:


  1. Мы вызываем setTimeout(function a(){}, 100)
  2. WebAPI откладывает задачу на 100мс
  3. Через 100мс, WebAPI кладет function a() в очередь (TaskQueue)
  4. Event Loop на нужный виток подбирает задачу

Нашему JS коду нужно как-то взаимодействовать с DOM. Считывать размеры элементов, добавлять свойства, рисовать какие-нибудь выпадающие элементы. Делать интерфейс живым.


Это накладывает ограничения на отрисовку элементов. Мы не можем просто так взять и запустить 2 потока, чтобы в одном выполнялся JS, а в другом CSS и отрисовка. Это потребовало бы большого количества синхронизаций кода, либо могло бы привести к неконсистентому исполнению. Поэтому и JS, и расчет отображения элементов (расположения, цвета и т.д.) работают в одном потоке. Значит в нашем Event Loop кроме JS есть "отрисовка". Давайте поместим ее в отдельную очередь и назовем render queue:



Итак, у нас есть две точки входа. Одна для большинства JS операций, другая для отрисовки. Теперь нам нужно разобраться, что же такое SomeJSTasks и как оно работает.


Для исполнения большей части JS кода у нас есть 2 очереди:


  • TaskQueue здесь почти все ивенты, отложенные задачи и т.п. Задача из этой очереди Task
  • MicroTaskQueue здесь обработка промисов и MutationObserver. Из этой очереди: MicroTask

Обновление экрана


Event loop неразрывно связан с обновлением экрана. В нем исполняется не только JS код, но и рассчитываются кадры страницы.


Браузеры стараются отображать изменения на странице максимально быстро. Здесь существует ряд ограничений:


  1. Хардварные ограничения: частота обновления экрана
  2. Софтверные ограничения: настройки ОС, браузера, настройки энергосбережения и т.д.

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


Для нашего Event Loop это означает, что на исполнение тасков отводится примерно 16.6мс (при 60FPS)


Что такое TaskQueue


Как только задачи набиваются в очередь Task, на каждом витке event loop мы достаем верхний таск и исполняем его. Если у нас остается достаточно времени, т.е. в render queue не появилось тасков, мы можем достать еще одни таск и т.д., пока не придет время для отрисовки нового кадра пользователю.


Разберем пару примеров:



У нас в очереди лежит 3 таска TaskA, TaskB, TaskC. Event Loop берет первый таск и исполняет. Он занимает 4 мс. Затем Event Loop проверяет другие очереди (Microtask queue и render queue) они пустые. Поэтому EventLoop исполняет второй таск. Второй таск занимает еще 12 мс. Итого 16 мс. Браузер добавляет в Render queue таски на отрисовку нового кадра, Event Loop берет эти таски. Они занимают примерно 1 мс. После этого Event Loop переходит обратно исполнять таски из TaskQueue.


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


Поэтому вот другой пример:



Здесь у нас только два таска в нашей очереди. Первый таск выполнялся 240 мс, так как 60FPS предполагает рендер кадра каждые 16.6 мс. А это означает, что мы пропустили примерно 14 кадров в эту секунду. Поэтому как только таск закончится, event loop начнет выполнять ожидающие таски из render queue, чтобы нарисовать кадр. Важный момент: то, что мы пропустили 14 кадров НЕ означает, что мы 15 раз будет рисовать кадры без остановки.


Прежде чем перейти к микротаскам, разберем такую штуку, как стек вызовов:


Стек вызовов


Стек вызовов это список, который отражает, какие функции сейчас вызываются и куда будет совершен переход, после окончания исполнения текущей функции.


Разберем пример:


function findJinny() {  debugger;  console.log('It seems you get confused with universe');}function goToTheCave() {  findJinny();}function becomeAPrince() {  goToTheCave();  }function findAFriend() {   // \_()_/}function startDndGame() {    const friends = [];  while (friends.length < 2) {    friends.push(findAFriend());  }  becomeAPrince();}console.log(startDndGame());

Мы запускаем этот код в консоли браузера и останавливаемся на дебаггере. Как будет выглядеть наш стек вызовов?


Мы начали наш стек из inline кода, значит самым верхом будет эта строчка. Как правило, в хром она будет указана просто ссылкой на нее. Обозначим ее inline. Дальше мы падаем в startDndGame, и вызываем несколько раз findAFriend. Однако он не попадет в итоговый callStack, потому что из всех findAFriend мы вышли. Итого наш стек вызовов будет выглядеть вот так:



Что же такое микротаски?


Микротаски ограничены. Это либо колбеки для promise, либо mutationObserver. Сама идея появления микротасок довольно костыльна, но она дает нам некоторые преимущества и недостатки по сравнению с TaskQueue.


Главная особенность микротасков: они будут исполнены сразу же, как только освобождается стек вызовов.


Например, в случае с этим стеком вызовов:



Если у нас в любой момент исполнения этого кода появляется fulfilled или rejected promise, то он будет исполнен сразу же, как только закончатся элементы в нашем стеке вызовов.


Любой JS код записывается в стек вызовов, что логично. Окончание стека вызовов, по сути, и есть окончание таска или микротаска. Интересный факт: Микротаски могут порождать другие микротаски, которые будут вызываться сразу же по освобождению стека вызовов. То есть рендер страницы может откладываться бесконечно. Это главный минус микротасков:



Если у нас 4 микротаски в очереди MicrotaskQueue, то они будут исполнены друг за другом, откладывая ререндер.


Недостаток микротасок также и их преимущество. Например, момент срабатывания MutationObserver это когда изменения на странице произошли, но пользователь их не увидит, пока колбек mutation observer не исполнится. Таким образом, мы можем эффективно управлять контентом, который видит пользователь.


Итого, наша картина event loop:



Мы уже разобрали, что таски могут следовать друг за другом, пропуская RenderQueue (если в ней нет тасок).


Но что исполняется внутри RenderQueue?


Рендер кадра можно поделить на несколько основных этапов. Каждый этап внутри может быть разделен на другие подэтапы (мы посмотрим это на примере Layout):



Остановимся на каждом этапе подробнее:


RequestAnimationFrame (raf)



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


Позволяет нам подписаться и запланировать изменения в DOM прямо перед отрисовкой страницы. Несколько важных фактов:


  1. Колбек для raf принимает аргумент: DOMHighResTimeStamp количество миллисекунд прошедших с начала жизни документа. Поэтому внутри колбека можно не брать время через perfomance.now, нужное время уже присутствует


  2. Аналогично setTimeout, raf возвращает дескриптор (id), поэтому запланированный raf можно отменить через cancelAnimationFrame.


  3. Если пользователь переключился на другой таб, свернул браузер перерисовки страницы не происходит. Raf тоже не вызывается


  4. JS код, который изменяет размеры элементов, считывает свойства, может зафорсить requestAnimationFrame


  5. Как посмотреть, как часто браузер обновляет кадр? Вот так:


    const checkRequestAnimationDiff = () => {let prev;function call() {    requestAnimationFrame((timestamp) => {        if (prev) {            console.log(timestamp - prev); // Должно быть в районе 16.6 мс, при 60FPS        }        prev = timestamp;        call();    });}call();}checkRequestAnimationDiff();
    

    Вот например, мой эксперимент (запускал на hh.ru):



  6. Сафари вызывает(ал) raf после отображения кадра, а не до. Пруф: https://github.com/whatwg/html/issues/2569#issuecomment-332150901



Style (recalculation)



Браузер пересчитывает стили, которые должны примениться из-за изменений, запланированных JS. Здесь же происходит вычисление активных media queries


Пересчет включает в себя как прямые изменения a.styles.left = '10px' так и те, которые описываются через CSS файлы, например element.classList.add('my-styles-class') Все они будут пересчитаны с точки зрения работы CSSOM и получения Render tree.


Если запустить профилировщик и открыть сайт hh.ru, вот тут можно найти время, потраченное на Style:



Layout



Вычисление слоев, расчет положения элементов на странице, их размеров, взаимного влияния друг на друга. Чем больше DOM элементов на странице, тем тяжелее эта операция.
Современные браузеры могут организовывать рендер и layout дерева по-разному. Например, в Хроме, кроме Layout в профилировщике вы увидите такие процессы как update layer tree и layout shift, который и отвечает за сдвиг элементов относительно друг друга.
Здесь на графике выделенная строка тот самый Layout сайта hh.ru при первом открытии приложения.



Layout это очень болезненная операция для современных веб-сайтов. Болезненна она потому, что наиболее тяжелый style recalculation происходит при первом рендере, а вот Layout происходит при:


  1. Чтении свойств влияющих на размер и положение элементов (offsetWidth, offsetLeft, getBoundingClientRect, и т.д.)
  2. При записи свойств, влияющих на размер и положение элементов, за исключением некоторых свойств, вроде transform и will-change. Для transform браузер задействует composition процесс, а в случае с will-change браузер попытается использовать composition. Вот здесь список актуальных причин.

Layout отвечает за:


  • Вычисление слоев
  • Расчет взаиморасположения элементов на слое
  • Расчет влияния одних элементов на другие

Layout (а вместе с ним raf и Style) может происходить не в свою очередь, когда нужно отрендерить страницу и применить изменения, а тогда, когда JS изменил размеры элементов или считал данные. Такой процесс называется force layout. Вот полный список свойств, который приводит браузер к остановке исполнения JS и вызову Layout.


div1.style.height = "200px"; // Изменили размер элементаvar height1 = div1.clientHeight; // Считываем его размер

Браузер не сможет рассчитать clientHeight нашего div1 без пересчета его реальных размеров. В этом случае, браузер приостановит исполнение JS кода (совсем) и выполнит по очереди: Style (чтобы определить, что изменять), Layout (чтобы определить, как изменилось). Layout должен рассчитать не только элементы, которые находятся перед, но и после div1. Современные браузеры оптимизируют расчет так, чтобы не пересчитывать абсолютно все дерево. Но в худшем случае этого не избежать. Процесс пересчета элементов называется Layout Shift. Вот так его можно посмотреть (справа список всех элементов, которые были сдвинуты и модифицированы во время Layout):



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


div1.style.height = "200px";var height1 = div1.clientHeight; // <-- layout 1div2.style.margin = "300px";var height2 = div2.clientHeight; // <-- layout 2 

В этом коде браузер запланировал изменение высоты div1 до 200px, но затем получил задачу на считывание. Пришлось сделать layout. Затем ситуация повторилась. Обратите внимание, браузер не произвел layout на операциях записи. Потому что в этот момент нужные данные у него уже были.


Давайте сгруппируем чтение и запись:


div1.style.height = "200px";div2.style.margin = "300px";var height1 = div1.clientHeight; // <-- layout 1var height2 = div2.clientHeight;

Такая группировка позволяет нам избежать второго layout, потому что браузер уже посчитал расположение элементов.


Layout работает со "слоями" в нашем потоке. Посмотреть на то, как браузер выделяет слои можно в chrome devtools -> More tools -> layers:



Таким образом, наш event loop превращается из одного витка в несколько, потому что и на этапе tasks, и на этапе microtasks мы можем запустить force layout:



Базовые советы для оптимизации layout:


  1. Уменьшать количество DOM нод
  2. По возможности избегать force layout
  3. Компоновать чтение и запись свойств

Paint



На этом шаге отрисовываем элементы, применяем стили color, background и т.д. Во время первого рендера сайта мы потратим на это достаточно времени:



В целом, эта операция не критична, так как в общем случае она не занимает много времени. Особенно после первого рендера.


После того, как пиксели нашей страницы обзавелись цветом, мы приступаем к следующему шагу Composition.


Composition



Это единственная операция, которая в классическом веб-сайте исполняется с помощью GPU. На этом этапе браузер исполняет специфические CSS стили, например transform.


Задача этой операции: совместить слои и получить готовый кадр.


Важное дополнение: само по себе свойство transform: translate не включает рендер элемента на видеокарте. То есть сделав transform: translateZ(0) вы не "перенесете элемент на видеокарту", это заблуждение.


Однако современные браузеры могут сами переносить часть операций на GPU. Лучше всего с актуальной информацией по этому вопросу знакомиться из исходников.



Именно с помощью transform советуют создавать сложные анимации. Секрет довольно прост:


  1. Анимация на transform позволяет нам не вызывать layout каждый кадр, мы экономим время
  2. Она позволяет нам избавиться от артефактов "мыльца" при анимациях, которые иногда бывают при анимировании left, rig ht, top, bottom

В то же время, верстать весь сайт только через transform не рекомендуется, так как это приводит к своим тормозам. Важно совмещать правильно верстку.


Как оптимизировать рендер?


Самая тяжелая операция для рендера кадра (для большинства сайтов) layout. Сверху на графиках был layout для рендера главной страницы hh.ru. При сложных анимациях каждый кадр может пересчитываться все элементы в DOM, а это означает, что каждый кадр вы будете тратить по 13-20 мс впустую. Это ведет к пропуску кадров и проблемам с производительностью вашего сайта.


Несколько примеров:



Мы можем пропустить Layout, если изменяем цвета, фоновое изображение и т.д.



Мы можем пропустить layout и paint, если изменения основаны на стиле transform и не затрагивают чтение свойств.


Итого, советы по оптимизации можно сгруппировать так:


  1. Выносите анимации на CSS. Исполнение JS кода не бесплатно
  2. Изменяйте transform свойство для перемещения объектов
  3. Используйте will-change свойство это свойство, которое позволяет браузерам "подготовить" дом элемент к изменениям определенных стилей. Важно это свойство помогает браузеру понять, что разработчик запланировал изменить. Это свойство нельзя применять к большому количеству элементов, иначе вы получите тормоза.
  4. Используйте батчевые изменения в DOM
  5. Используйте requestAnimationFrame для планирования изменений в следующем кадре
  6. Комбинируйте задачи на запись \ чтение свойств элементов. Обращайте внимание на вызовы свойств, которые форсят layout.
  7. При возникновении сложных ситуаций, лучше всего запустить профилировщик и посмотреть на частоту и время вызовов. Это даст вам информацию о том, какой этап тормозит, оптимизируйте каждый этап отдельно.

Вместо заключения


В этой части мы разобрали каким образом работает Runtime в нашем браузере, его сильные и слабые стороны. Это позволяет нам:


  1. Понимать, как лучше писать код
  2. Знать в какую сторону смотреть при появлении проблем

Также мы получили достаточно глубокое понимание event loop и его составляющих:


Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru