Skip to content

Instantly share code, notes, and snippets.

@nikolai-shabalin
Last active July 30, 2023 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nikolai-shabalin/638730a5a3df21066c215d58fb835308 to your computer and use it in GitHub Desktop.
Save nikolai-shabalin/638730a5a3df21066c215d58fb835308 to your computer and use it in GitHub Desktop.
Пассивные слышатели

Пассивные слушатели событий

Пассивные слушатели событий - это новая возможность в спецификации DOM, которая позволяет разработчикам перейти к использованию более эффективной прокрутке, устранив необходимость блокировки прокрутки в слушателях событий касания и перемещения колеса. Разработчики могут аннотировать слушатели касаний и колес с помощью {passive: true}, чтобы указать, что они никогда не будут вызывать preventDefault. Эта возможность появилась в Chrome 51, Firefox 49 и появилась в WebKit. Посмотрите на видео ниже, чтобы увидеть пассивные слушатели событий в действии:

демонстрационное видео

Проблема

Плавная прокрутка - важнейшее условие комфортной работы в Интернете, особенно на сенсорных устройствах. Во всех современных браузерах реализована функция потоковой прокрутки, позволяющая плавно выполнять прокрутку даже при работе дорогостоящего JavaScript, однако эта оптимизация частично сводится на нет необходимостью дожидаться результатов работы обработчиков touchstart и touchmove, которые могут полностью предотвратить прокрутку, вызвав для события preventDefault(). Хотя существуют определенные сценарии, в которых автор действительно может захотеть предотвратить прокрутку, анализ показывает, что большинство обработчиков событий касания в Интернете никогда не вызывают функцию preventDefault(), поэтому браузеры часто блокируют прокрутку без необходимости. Например, в Chrome для Android 80% событий касания, блокирующих прокрутку, на самом деле никогда не предотвращают ее. 10% этих событий добавляют более 100 мс задержки к началу прокрутки, а катастрофическая задержка не менее 500 мс происходит в 1% случаев прокрутки.

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

Фундаментальная проблема здесь не ограничивается событиями касания. Cобытие wheel страдают от аналогичной проблемы. В отличие от этого, обработчики событий указателя предназначены для того, чтобы никогда не задерживать прокрутку (хотя разработчики могут декларативно запретить прокрутку вообще с помощью CSS-свойства touch-action), поэтому не страдают от этой проблемы. По сути, предложение пассивного слушателя событий позволяет перенести свойства событий указателя на события касания и колеса.

Это предложение предоставляет авторам возможность указать при регистрации обработчика, может ли он вызывать preventDefault() для данного события (т.е. нужно ли ему событие, которое является отменяемым (cancelable)). Если в конкретной точке ни один обработчик касания или колеса не требует отменяемого события, то пользовательский агент может сразу начать прокрутку, не дожидаясь JavaScript. То есть пассивные слушатели избавлены от неожиданных побочных эффектов, связанных с производительностью.

EventListenerOptions

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

document.addEventListener('touchstart', handler, true);

EventListenerOptions позволяет нам записать это более явно:

document.addEventListener('touchstart', handler, {capture: true});

Это просто новый (расширяемый) синтаксис для существующего поведения - с указанием нужно ли вызывать слушателя на этапе захвата или на этапе всплытия.

Решение: опция 'passive'.

Теперь, когда у нас есть расширяемый синтаксис для указания опций во время регистрации обработчика события, мы можем добавить новую опцию passive, которая заранее заявляет, что слушатель никогда не будет вызывать preventDefault() для данного события. Если это произойдет, то пользовательский агент просто проигнорирует запрос (в идеале - выдаст хотя бы консольное предупреждение), как это уже происходит для событий с Event.cancelable=false. Разработчик может убедиться в этом, запросив Event.defaultPrevented до и после вызова preventDefault(). Например:

addEventListener(document, "touchstart", function(e) {
  console.log(e.defaultPrevented);  // будет ложным
  e.preventDefault();   // ничего не делает, поскольку слушатель пассивен
  console.log(e.defaultPrevented);  // по-прежнему ложный
}, Modernizr.passiveeventlisteners ? {passive: true} : false);

Теперь вместо того, чтобы блокировать прокрутку при наличии любого слушателя касания или колеса, браузеру нужно делать это только при наличии непассивных слушателей (см. TouchEvents spec). Пассивные (passive) слушатели не имеют побочных эффектов, связанных с производительностью.

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

Обнаружение особенностей

Поскольку в старых браузерах любой объект в третьем аргументе будет интерпретироваться как true для аргумента capture, разработчикам важно при использовании этого API использовать функцию обнаружения особенностей или polyfill, чтобы избежать непредвиденных результатов. Обнаружение особенностей для конкретных опций может быть выполнено следующим образом:

// Проверка через геттер в объекте options на доступ к пассивному свойству
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function() {
      supportsPassive = true;
    }
  });
  window.addEventListener("testPassive", null, opts);
  window.removeEventListener("testPassive", null, opts);
} catch (e) {}

// Использовать результаты нашего детекта. passive применяется, если поддерживается, capture будет false в любом случае.
elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

Для упрощения этой задачи можно использовать функцию detect из Detect It, например:

elem.addEventListener('touchstart', fn, 
  detectIt.passiveEvents ? {passive:true} : false);

В Modernizr также есть detect здесь. Существует дискуссия по открытым стандартам вокруг предоставления более простого API для обнаружения признаков членов словаря.

Устранение необходимости отмены событий

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

  • Панорамирование и масштабирование карты
  • Полноэкранные игры.

В этих случаях текущее поведение (предотвращающее оптимизацию прокрутки) вполне адекватно, поскольку сама прокрутка предотвращается последовательно. В этих случаях нет необходимости использовать пассивные слушатели, хотя часто целесообразно применить CSS-правило touch-action: none, чтобы явно выразить свои намерения (например, для поддержки браузеров с событиями Pointer, но не Touch).

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

  • мониторинг активности пользователя, который просто хочет знать, когда пользователь был активен в последний раз.
  • обработчики touchstart, скрывающие некоторые активные элементы пользовательского интерфейса (например, всплывающие подсказки)
  • обработчики touchstart и touchend, которые стилизуют элементы пользовательского интерфейса (без подавления события click).

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

Существует несколько более сложных сценариев, когда обработчик хочет подавить прокрутку только при определенных условиях, например:

  • Проведение пальцем по горизонтали для поворота карусели, удаления элемента или открытия ящика, при этом вертикальная прокрутка остается разрешенной.
    • В этом случае используйте touch-action: pan-y, чтобы декларативно запретить прокрутку, начинающуюся вдоль горизонтальной оси, без необходимости вызова preventDefault() (тестовая страница).
    • Для корректной работы во всех браузерах вызов preventDefault должен быть обусловлен отсутствием поддержки конкретного используемого правила touch-action (отметим, что Safari 9 в настоящее время поддерживает только touch-action: manipulation).
  • Элемент пользовательского интерфейса (например, слайдер громкости YouTube), который скользит по событиям горизонтального колеса без изменения поведения прокрутки по событиям вертикального колеса. Поскольку эквивалента "touch-action" для событий от колес не существует, этот случай может быть реализован только с помощью непассивных слушателей колес.
  • Модели делегирования событий, когда код, добавляющий слушатель, не знает, отменит ли потребитель событие.
    • Одним из вариантов здесь является раздельное делегирование для пассивных и непассивных слушателей (как если бы это были разные типы событий).
    • Также можно использовать touch-action, как описано выше (рассматривая события Touch как события Pointer Events.

Отладка и измерение преимуществ

Вы можете получить краткое представление о возможных преимуществах (и потенциальных поломках), если заставите слушателей касания/колеса считаться пассивными с помощью параметра chrome://flags/#passive-listener-default (новый в Chrome 52) Это облегчает проведение собственных сравнений, как в этом популярном видео.

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

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

Сокращение и разрушение долго выполняющегося JS по-прежнему критически важно

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

Дальнейшее чтение и обсуждение

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

Оригинал

Это перевод - https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment