[Перевод] Устройство современного веб-браузера Chrome (часть 4/4)
Это последний пост из серии 4-х постов, посвященной заглядыванию внутрь Chrome, и исследующей, как он обрабатывает наш код для отображения веб-сайта. В предыдущем посте мы рассмотрели *рендер-процесс (renderer process) и узнали о *композ-потоке (compositor thread). В этом посте мы рассмотрим, как *композ-поток обеспечивает плавное взаимодействие при вводе данных пользователем.
Часть 1
Часть 2
Часть 3
Часть 4 (текущая)
- в ходе перевода, я старался вычленять из статьи ПОНЯТИЯ, т.е. текстовые единицы которые несут специальный (технический) смысл. В переводе эти понятия выделены по особенному — во первых понятия предваряются символом звёздочки, во-вторых в них вместо пробела используется тире. Например: *браузер-процесс, *сайто-изоляция. При переводе понятий, приоритет отдавался не красоте перевода, а желанию выделить, акцентировать, то что мы имеем дело с ПОНЯТИЕМ, а не с фигурой речи.
- также, некоторые слова переведены неверно с точки зрения русского языка, в жаргонном стиле, например пайплайн, продакшен. У «технарей» такой перевод не вызовет затруднений, у остальных читателей прошу прощения.
Когда вы слышите «события ввода», вы можете подумать только о вводе в текстовое поле или щелчке мышью, но с точки зрения браузера, ввод означает любой жест пользователя. Прокрутка колеса мыши — это событие ввода, и нажатие или наведение мыши также является событием ввода.
Когда происходит пользовательский жест, например, прикосновение к экрану, *браузер-процесс — является тем процессом, который получает этот жест первым. Однако *браузер-процесс знает только о том, где этот жест произошел, поскольку содержимое внутри вкладки обрабатывается *рендер-процессом. Так что *браузер-процесс посылает в *рендер-процесс тип события (например touchstart) и его координаты. *Рендер-процесс соответствующим образом обрабатывает событие, находя цель события и запуская слушателей события, которые прикреплены к нему.
Рисунок 1: Событие ввода проходит через *браузер-процесс в *рендер-процесс
В предыдущем посте мы рассмотрели, как *композ-поток может делать прокрутку плавной, создавая растровые слои. Если к странице не подключены слушатели входных событий, *композ-поток может создать новую кадр-композицию, полностью независимую от основной. Но что делать, если к странице прикреплены слушатели событий? Как *композ-поток узнает, нужно ли обрабатывать событие?
Рисунок 2: Viewport наводится на слои страницы
Поскольку выполнение JavaScript является задачей главного потока, при компоновке страницы *композ-поток отмечает область страницы, к которой прикреплены обработчики событий как «Non-Fast Scrollable Region» (*нбс-регион). Обладая этой информацией, *композ-поток может гарантировать, что входное событие будет отправлено в главный поток, если событие произойдет в этом регионе. Если входное событие приходит из-за пределов этого региона, то *композ-поток продолжает композицию нового кадра, не дожидаясь главного потока.
Рисунок 3: Схема описывающая ввод в *нбс-регион
= Знайте когда вы пишете обработчики событий
Общей моделью обработки событий в веб-разработке является делегирование событий. Так как события всплывают, вы можете прикрепить одного обработчика событий к самому верхнему элементу и делегировать задачи в зависимости от цели события. Возможно, вы видели или писали код, подобный приведенному ниже.
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault();
}
});
Так как для всех элементов необходимо написать только один обработчик события, использование этого шаблона делегирования событий привлекательно. Однако, если посмотреть на этот код с точки зрения браузера, то теперь вся страница помечена как *нбс-регион. Это означает, что даже если ваше приложение не интересуют данные вводимые в определенных частях страницы, *композ-поток должен взаимодействовать с главным потоком и ждать его каждый раз, когда приходит входящее событие. Таким образом, способность композитора к плавной прокрутке оказывается подорванной.
Рисунок 4: Схема описывающая ввод в *нбс-регион покрывающий всю страницу
Для того, чтобы это не произошло, вы можете передать опцию passive:true
в вашем слушателе событий. Это подскажет браузеру, что вы все еще хотите слушать событие в главном потоке, а *композ-поток сможет продолжить композировать новый кадр.
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
Представьте, что на странице есть поле, ограничивающее направление прокрутки только горизонталью.
Использование опции passive: true
в событии курсора означает, что прокрутка страницы может быть гладкой, но вертикальная прокрутка может начаться к тому времени, когда вы хотите выполнить preventDefault
для ограничения направления прокрутки. Вы можете проверить это, используя метод event.cancelable
.
Рисунок 5: Веб-страница с частью страницы у которой скролл только горизонтальный
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // блокировка нативного скрола
/*
* сделать то что вам нужно
*/
}
}, {passive: true});
Альтернативно, вы можете использовать CSS-правило, такое как touch-action
, чтобы полностью устранить обработчик события.
#area {
touch-action: pan-x;
}
Когда *композ-поток посылает входное событие в главный поток, первым делом выполняется проверка попадания в цель события. Проверка попадания использует *записи-отрисовки (paint records), которые были сгенерированы в процессе рендеринга, чтобы выяснить, что находится под координатами точки, в которой произошло событие.
Рисунок 6: Главный поток ищет *записи-отрисовки запрашивая что нарисовано в точке x.y
В предыдущем посте мы обсуждали, что наш типичный дисплей обновляет экран 60 раз в секунду и что нам нужно идти в ногу с частотой кадров для плавной анимации. При вводе данных типичное устройство с сенсорным экраном передает события 60–120 раз в секунду, а типичная мышь передает события 100 раз в секунду. Событие ввода имеет более высокую точность, чем может обновить наш экран.
Если непрерывное событие, подобное сенсорному движению, посылается в основной поток 120 раз в секунду, то оно может вызвать избыточное количество проверок на попадание и выполнение JavaScript по сравнению с тем, как не быстро может обновляться экран.
Рисунок 7: События забивают временнУю ленту кадров что приводит к дрожанию страницы
Для минимизации излишних вызовов главного потока, Chrome объединяет (coalesces, *коалесирует) непрерывные события (такие как wheel, mousewheel, mousemove, pointermove, touchmove) и задерживает диспетчеризацию до момента, непосредственно предшествующего следующему requestAnimationFrame.
Рисунок 8: Та же лента, но события *коалесируются и придерживаются
Любые дискретные события, такие как нажатие keydown, keyup, mouseup, mousedown, touchstart и touchchend отправляются немедленно.
Для большинства веб-приложений, *коалесирования событий должно быть достаточно, чтобы обеспечить хороший пользовательский опыт. Однако, если вы строите такие вещи, как приложение для рисования и прокладываете путь на основе координат touchmove
, вы можете потерять промежуточные координаты для отрисовки гладкой линии. В этом случае вы можете использовать метод getCoalescedEvents
в событии курсора, чтобы получить информацию об этих *коалесированных событиях.
Рисунок 9: Плавный путь от жеста касания слева, рваный из-за *коалесирований пути справа
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
/* отрисовка линии используя координаты x и y */
}
});
В этой серии постов мы рассмотрели внутреннюю работу веб-браузера. Если вы никогда не задумывались о том, почему DevTools рекомендует добавлять {passive: true}
в обработчик событий или зачем писать атрибут async
в теге сценария, я надеюсь, что эта серия прольёт некоторый свет на то, почему браузеру нужна эта информация для обеспечения более быстрой и плавной работы с веб.
= Использование Lighthouse
Если вы хотите, чтобы ваш код был удобен для браузера, но не знаете, с чего начать, то рекомендую Lighthouse — это инструмент, который проводит аудит любого сайта и даёт вам отчет о том, что делается правильно и что нуждается в улучшении. Читая список аудитов, вы также получаете представление о том, о чем беспокоится браузер.
= Изучаем как измерять производительность
Настройки производительности могут отличаться для разных сайтов, поэтому очень важно, чтобы вы измерили производительность вашего сайта и решили, что лучше всего подходит для него. Команда Chrome DevTools имеет несколько руководств по оценке производительности вашего сайта.
= Добавление Feature Policy на ваш сайт
Если вы хотите сделать дополнительный шаг, Feature Policy — это новая функция веб-платформы, которая может быть ограждением для вас, когда вы строите свой проект. Включение Feature Policy гарантирует определенное поведение вашего приложения и предотвращает ошибки. Например, если вы хотите гарантировать, что ваше приложение никогда не будет блокировать парсинг, вы можете запустить ваше приложение на основе политики синхронных сценариев. При включённом sync-script: 'none'
, блокирование JavaScript-ом парсера будет отключено. Это не позволит ни одному вашему коду заблокировать парасер, а браузеру будет не нужно беспокоиться о том, чтобы ставить парсер на паузу.
Когда я начинала создавать сайты, меня волновало только то, как я буду писать свой код и что поможет мне быть более продуктивной. Это важно, но мы также должны думать о том, как браузер принимает код, который мы пишем. Современные браузеры вкладывали и продолжают вкладывать средства в то, чтобы предоставить пользователям более качественный веб-интерфейс. Будьте любезны с браузером, организовывая наш код, и тем самым также, улучшая свой пользовательский опыт. Надеюсь, Вы присоединитесь ко мне в стремлении быть добрее к браузерам!
Огромное спасибо всем, кто рецензировал ранние проекты этой серии, включая (но не ограничиваясь): Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov, и Charlie Reis.
Вам понравилась эта серия? Если у вас есть вопросы или предложения для будущих постов, я бы хотела услышать их от вас в разделе комментариев ниже или у меня в твиттере — @kosamari.
Часть 1
Часть 2
Часть 3
Часть 4 (текущая)