события прокрутки: requestAnimationFrame VS requestIdleCallback VS пассивные прослушиватели событий

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

Однако я часто находил библиотеки и статьи, в которых такие влиятельные люди, как Пол Льюис, рекомендуют использовать requestAnimationFrame. Однако, поскольку веб-платформа быстро развивается, возможно, что некоторые советы со временем устареют.

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

Я вижу 3 основных инструмента, которые могут изменить ситуацию с точки зрения UX:

Итак, я хотел бы знать, для каждого варианта использования (у меня есть только 2, но вы можете придумать другие), какой инструмент мне следует использовать прямо сейчас, чтобы получить очень хороший опыт прокрутки?

Чтобы быть более точным, мой главный вопрос был бы больше связан с бесконечной прокруткой и разбиением на страницы (которые обычно не должны запускать визуальную анимацию, но нам нужна хорошая прокрутка), лучше ли заменить requestAnimationFrame комбинацией requestIdleCallback + обработчик событий пассивной прокрутки? Мне также интересно, когда имеет смысл использовать requestIdleCallback для вызова API или обработки ответа API, чтобы прокрутка работала лучше, или это то, что браузер уже может обрабатывать для нас?


person Sebastien Lorber    schedule 19.01.2017    source источник
comment
Мне нравится этот вопрос, но я боюсь, что без некоторых фрагментов кода большинство людей боятся начинать самоуверенные дебаты... у вас есть время привести несколько примеров?   -  person deblocker    schedule 15.02.2017
comment
@op, можете ли вы предоставить пример фрагмента, демонстрирующего функцию прокрутки, как вы хотите, чтобы люди могли использовать ее в ответе? Тогда я мог бы рассмотреть возможность назначить за него награду. Сейчас это слишком широко.   -  person Tschallacka    schedule 23.05.2017
comment
@Tschallacka, как объяснялось выше, я не ищу конкретно один вариант использования, а ищу, как принимать решения для каждого варианта использования. Самый простой и распространенный вариант использования, который вы можете придумать, это, вероятно, какой-то вид с бесконечной прокруткой, как в Instagram, Twitter или Facebook.   -  person Sebastien Lorber    schedule 23.05.2017
comment
@SebastienLorber Это делает ваш вопрос невероятно широким, и все браузеры имеют разные реализации и оптимизации. Если ваш вопрос не будет сужен до конкретных деталей, он может оказаться закрытым как слишком широкий.   -  person Tschallacka    schedule 23.05.2017


Ответы (1)


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

В общем, все инструменты, о которых вы просили (rAF, rIC и пассивные слушатели), являются отличными инструментами и не исчезнут в ближайшее время. Но вы должны знать, зачем их использовать.

Прежде чем я начну: если вы создаете синхронизированные/связанные с прокруткой эффекты, такие как эффекты параллакса/липкие элементы, дросселирование с использованием rIC, setTimeout не имеет смысла, потому что вы хотите реагировать немедленно.

requestAnimationFrame

rAF дает вам точку внутри жизненного цикла кадра прямо перед тем, как браузер захочет вычислить новый стиль и макет документа. Вот почему он идеально подходит для анимации. Во-первых, он не будет вызываться чаще или реже, чем рассчитывал браузер (правильная частота). Во-вторых, он вызывается прямо перед тем, как браузер вычислит макет (правильное время). На самом деле использование rAF для любых изменений макета (изменения DOM или CSSOM) имеет большой смысл. rAF синхронизируется с V-SYNC, как и любой другой материал, связанный с отображением макета в браузере. .

использование rAF для газа/устранения дребезга

Пример Пола Льюиса по умолчанию выглядит так:

var scheduledAnimationFrame;
function readAndUpdatePage(){
  console.log('read and update');
  scheduledAnimationFrame = false;
}

function onScroll (evt) {

  // Store the scroll value for laterz.
  lastScrollY = window.scrollY;

  // Prevent multiple rAF callbacks.
  if (scheduledAnimationFrame){
    return;
  }

  scheduledAnimationFrame = true;
  requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll', onScroll);

Этот шаблон очень часто используется/копируется, хотя на практике он практически не имеет смысла. (И я задаюсь вопросом, почему ни один разработчик не видит этой очевидной проблемы.) В общем, теоретически имеет большой смысл дросселировать все хотя бы до rAF, потому что больше запрашивать изменения макета у браузера не имеет смысла. чаще, чем браузер отображает макет.

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

На практике вы можете проверить то, что я только что сказал, добавив console.log и проверить, как часто этот шаблон «предотвращает множественные обратные вызовы rAF» (ответ — нет, иначе это будет ошибка браузера).

  // Prevent multiple rAF callbacks.
  if (scheduledAnimationFrame){
    console.log('prevented rAF callback');
    return;
  }

Как вы увидите, этот код никогда не выполняется, это просто мертвый код.

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

//declare box, element, pos
function writeLayout(){
    element.classList.add('is-foo');
}

window.addEventListener('scroll', ()=> {
    box = element.getBoundingClientRect();

    if(box.top > pos){
        requestAnimationFrame(writeLayout);
    }
});

С помощью этого паттерна вы можете успешно уменьшить или даже полностью устранить тряску макета. Идея проста: внутри вашего слушателя прокрутки вы читаете макет и решаете, нужно ли вам модифицировать DOM, а затем вызываете функцию, которая модифицирует DOM, используя rAF. Почему это полезно? rAF гарантирует, что вы переместите недействительность макета (в конце кадра). Это означает, что любой другой код, который вызывается внутри того же фрейма, работает с допустимым макетом и может работать со сверхбыстрыми методами чтения макета.

Этот паттерн на самом деле настолько хорош, что я бы предложил следующий вспомогательный метод (написанный на ES5):

/**
 * From https://stackoverflow.com/a/44779316
 *
 * @param {Function} fn Callback function
 * @param {Boolean|undefined} [throttle] Optionally throttle callback
 * @return {Function} Bound function
 *
 * @example
 * //generate rAFed function
 * jQuery.fn.addClassRaf = bindRaf(jQuery.fn.addClass);
 *
 * //use rAFed function
 * $('div').addClassRaf('is-stuck');
 */
function bindRaf(fn, throttle) {
  var isRunning;
  var that;
  var args;

  var run = function() {
    isRunning = false;
    fn.apply(that, args);
  };

  return function() {
    that = this;
    args = arguments;

    if (isRunning && throttle) {
      return;
    }

    isRunning = true;
    requestAnimationFrame(run);
  };
}

requestIdleCallback

По API похож на rAF, но дает что-то совершенно другое. Это дает вам несколько периодов простоя внутри кадра. (Обычно это происходит после того, как браузер рассчитал макет и выполнил отрисовку, но до выполнения вертикальной синхронизации еще остается время.) Даже если страница отстает от просмотра пользователями, могут быть некоторые кадры, где браузер холостой ход. Хотя rIC может дать вам макс. 50 мс. Большую часть времени у вас есть только от 0,5 до 10 мс для выполнения вашей задачи. Из-за того, что в какой момент жизненного цикла фрейма вызываются rIC обратные вызовы, вам не следует изменять DOM (используйте для этого rAF).

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

Живые примеры для debounce и дроссель.)

Вот также статья о rAF, которая включает две диаграммы, которые могут помочь понять различные моменты внутри «жизненного цикла кадра».

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

Пассивные прослушиватели событий были изобретены для повышения производительности прокрутки. Современные браузеры переместили прокрутку страницы (рендеринг прокрутки) из основного потока в поток композиции. (см. https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/)

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

Это означает, что как только один из этих прослушивателей событий привязан, браузер должен дождаться выполнения этого прослушивателя, прежде чем браузер сможет вычислить прокрутку. Эти события в основном touchstart, touchmove, touchend, wheel и теоретически в некоторой степени keypress и keydown. Само событие scroll не является одним из этих событий. Событие scroll не имеет действия по умолчанию, которое можно предотвратить с помощью сценария.

Это означает, что если вы не используете preventDefault в своих touchstart, touchmove, touchend и/или wheel, всегда используйте пассивные прослушиватели событий, и все будет в порядке.

Если вы используете preventDefault, проверьте, можете ли вы заменить его свойством CSS touch-action или понизить его, по крайней мере, в вашем дереве DOM (например, нет делегирования событий для этих событий). В случае слушателей wheel вы можете привязать/отвязать их на mouseenter/mouseleave.

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

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

Резюме

Чтобы ответить на ваш вопрос

  • для ленивой загрузки, бесконечного просмотра используйте комбинацию setTimeout + requestIdleCallback для ваших прослушивателей событий и используйте rAF для любой записи макета (мутации DOM).
  • для мгновенных эффектов по-прежнему используйте rAF для любой записи макета (мутации DOM).
person alexander farkas    schedule 27.06.2017
comment
Вот это да. Такой отличный ответ. Возможно, это должен был быть пост в блоге. - person powerbuoy; 27.06.2017
comment
Спасибо, отличный ответ. Не совсем уверен насчет the scroll event is triggered every time the browser renders a scroll position change: для меня вполне вероятно, что обратный вызов rAF всегда запускается до следующего события прокрутки, поэтому журнал не создается - person Sebastien Lorber; 29.06.2017
comment
Кроме того, для таких вещей, как липкий заголовок или кнопка scrollTop, мне кажется, имеет смысл использовать пассивный прослушиватель событий для события прокрутки, нет? Мы не будем препятствовать прокрутке и не обязательно нуждаемся в обратной связи 16 мс для таких функций (в отличие от параллакса). - person Sebastien Lorber; 29.06.2017
comment
@SebastienLorber: Не понимаю вашего возражения против события rAF/scroll. Как я описал в своем ответе, rAF синхронизируется с V-Sync, а рендеринг прокрутки также синхронизируется с V-Sync. Так что да, rAF всегда вызывается перед следующим событием прокрутки, и дросселирование имеет смысл только в том случае, если что-то имеет другую частоту (медленнее), чем другое. Тот факт, что вы не видите консоли, демонстрирует бесполезность этого шаблона. - person alexander farkas; 30.06.2017
comment
@SebastienLorber: о пассивном прослушивателе событий и событии scroll. Событие scroll не является cancelable, это означает, что даже если вы вызываете preventDefault, оно не имеет никакого эффекта. Браузер не делает ничего другого, если вы используете пассивный вариант. - person alexander farkas; 30.06.2017
comment
О, хорошо, я не знал, я думал, что изменение прокрутки было поведением по умолчанию, происходящим после события прокрутки, но имеет смысл сначала выполнить рендеринг, а затем запустить прокрутку, чтобы мы могли получить новую позицию прокрутки и сделать некоторые математические расчеты... - person Sebastien Lorber; 30.06.2017
comment
Я бы сказал, что этот шаблон встречается так часто, потому что его можно найти на странице MDN: прокрутка, оптимизация прокрутки с помощью window.requestAnimationFrame и MDN обычно считаются надежным источником. Так что если ваше утверждение верно. Тогда нужно предлагать поправку на странице MDN? - person t.niese; 05.04.2018
comment
На странице MDN действительно говорится, что эта оптимизация «часто не нужна», поскольку rAF и Scroll срабатывают «примерно с одинаковой скоростью». - person blackmamba; 18.04.2018
comment
Я спорю с коллегой о том, влияет ли создание пассивного обработчика событий на то, как быстро он вызывается (т.е. влияет на приоритет обработчика). Я понимаю, что это не так. Я прав? - person sming; 01.11.2018
comment
Красиво объясненный ответ. В инете ничего лучше этого не нашел. Ты Бог. - person Sayan J. Das; 27.03.2020
comment
@alexanderfarkas Вы утверждаете, что [шаблон Пола Льюиса] практически не имеет смысла. Хорошо, я это понимаю. Но когда я отлаживаю isRunning вместе с событием прокрутки, я заметил, что это также никогда не соответствует действительности. - person Coli; 12.10.2020