Это последний пост из серии 4-х постов, посвященной заглядыванию внутрь Chrome, и исследующей, как он обрабатывает наш код для отображения веб-сайта. В предыдущем посте мы рассмотрели *рендер-процесс (renderer process) и узнали о *композ-потоке (compositor thread). В этом посте мы рассмотрим, как *композ-поток обеспечивает плавное взаимодействие при вводе данных пользователем.



Часть 1
Часть 2
Часть 3
Часть 4 (текущая)


Об особенностях перевода
  • в ходе перевода, я старался вычленять из статьи ПОНЯТИЯ, т.е. текстовые единицы которые несут специальный (технический) смысл. В переводе эти понятия выделены по особенному — во первых понятия предваряются символом звёздочки, во-вторых в них вместо пробела используется тире. Например: *браузер-процесс, *сайто-изоляция. При переводе понятий, приоритет отдавался не красоте перевода, а желанию выделить, акцентировать, то что мы имеем дело с ПОНЯТИЕМ, а не с фигурой речи.
  • также, некоторые слова переведены неверно с точки зрения русского языка, в жаргонном стиле, например пайплайн, продакшен. У "технарей" такой перевод не вызовет затруднений, у остальных читателей прошу прощения.

События ввода из виджетов браузера


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


Когда происходит пользовательский жест, например, прикосновение к экрану, *браузер-процесс — является тем процессом, который получает этот жест первым. Однако *браузер-процесс знает только о том, где этот жест произошел, поскольку содержимое внутри вкладки обрабатывается *рендер-процессом. Так что *браузер-процесс посылает в *рендер-процесс тип события (например touchstart) и его координаты. *Рендер-процесс соответствующим образом обрабатывает событие, находя цель события и запуская слушателей события, которые прикреплены к нему.


image


Рисунок 1: Событие ввода проходит через *браузер-процесс в *рендер-процесс


*Композ-поток получает события ввода


В предыдущем посте мы рассмотрели, как *композ-поток может делать прокрутку плавной, создавая растровые слои. Если к странице не подключены слушатели входных событий, *композ-поток может создать новую кадр-композицию, полностью независимую от основной. Но что делать, если к странице прикреплены слушатели событий? Как *композ-поток узнает, нужно ли обрабатывать событие?


image
Рисунок 2: Viewport наводится на слои страницы


Понятие *нбс-региона


Поскольку выполнение JavaScript является задачей главного потока, при компоновке страницы *композ-поток отмечает область страницы, к которой прикреплены обработчики событий как "Non-Fast Scrollable Region" (*нбс-регион). Обладая этой информацией, *композ-поток может гарантировать, что входное событие будет отправлено в главный поток, если событие произойдет в этом регионе. Если входное событие приходит из-за пределов этого региона, то *композ-поток продолжает композицию нового кадра, не дожидаясь главного потока.


image


Рисунок 3: Схема описывающая ввод в *нбс-регион


= Знайте когда вы пишете обработчики событий


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


document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

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


image


Рисунок 4: Схема описывающая ввод в *нбс-регион покрывающий всю страницу


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


document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

Проверка является ли событие cancelable


Представьте, что на странице есть поле, ограничивающее направление прокрутки только горизонталью.


Использование опции passive: true в событии курсора означает, что прокрутка страницы может быть гладкой, но вертикальная прокрутка может начаться к тому времени, когда вы хотите выполнить preventDefault для ограничения направления прокрутки. Вы можете проверить это, используя метод event.cancelable.


image
Рисунок 5: Веб-страница с частью страницы у которой скролл только горизонтальный


document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // блокировка нативного скрола
        /*
        * сделать то что вам нужно
        */
    }
}, {passive: true});

Альтернативно, вы можете использовать CSS-правило, такое как touch-action, чтобы полностью устранить обработчик события.


#area {
  touch-action: pan-x;
}

Поиск цели события


Когда *композ-поток посылает входное событие в главный поток, первым делом выполняется проверка попадания в цель события. Проверка попадания использует *записи-отрисовки (paint records), которые были сгенерированы в процессе рендеринга, чтобы выяснить, что находится под координатами точки, в которой произошло событие.


image


Рисунок 6: Главный поток ищет *записи-отрисовки запрашивая что нарисовано в точке x.y


Минимизация распространения события в главный поток


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


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


image


Рисунок 7: События забивают временнУю ленту кадров что приводит к дрожанию страницы


Для минимизации излишних вызовов главного потока, Chrome объединяет (coalesces, *коалесирует) непрерывные события (такие как wheel, mousewheel, mousemove, pointermove, touchmove) и задерживает диспетчеризацию до момента, непосредственно предшествующего следующему requestAnimationFrame.


image


Рисунок 8: Та же лента но события *коалесируются и придерживаются


Любые дискретные события, такие как нажатие keydown, keyup, mouseup, mousedown, touchstart и touchchend отправляются немедленно.


Использование getCoalescedEvents для получения межкадровых событий


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


image


Рисунок 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-ом парсера будет отключено. Это не позволит ни одному вашему коду заблокировать парасер, а браузеру будет не нужно беспокоиться о том, чтобы ставить парсер на паузу.


Итог


Когда я начинала создавать сайты, меня волновало только то, как я буду писать свой код и что поможет мне быть более продуктивной. Это важно, но мы также должны думать о том, как браузер принимает код, который мы пишем. Современные браузеры вкладывали и продолжают вкладывать средства в то, чтобы предоставить пользователям более качественный веб-интерфейс. Будьте любезны с браузером, организовывая наш код, и тем самым также, улучшая свой пользовательский опыт. Надеюсь, Вы присоединитесь ко мне в стремлении быть добрее к браузерам!


image


Огромное спасибо всем, кто рецензировал ранние проекты этой серии, включая (но не ограничиваясь): Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov, и Charlie Reis.


Вам понравилась эта серия? Если у вас есть вопросы или предложения для будущих постов, я бы хотела услышать их от вас в разделе комментариев ниже или у меня в твиттере — @kosamari.


Часть 1
Часть 2
Часть 3
Часть 4 (текущая)