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

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

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

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

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

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

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

Ответ 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 все на хотя бы 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):

/**
 * @param fn {Function}
 * @param [throttle] {Boolean|undefined}
 * @return {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, that, 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 но дает что-то совершенно другое. Это дает вам несколько периодов простоя внутри кадра. (Обычно точка после того, как браузер вычислил макет и выполнил рисование, но до v-синхронизации все еще остается некоторое время.) Даже если страница запаздывает с точки зрения пользователей, могут быть некоторые кадры, в которых браузер находится холостой ход. Хотя rIC может дать вам макс. 50мс. Большую часть времени у вас есть только от 0,5 до 10 мс для выполнения вашей задачи. В связи с тем, что в какой-то момент в жизненном цикле кадра rIC обратные вызовы rIC, вам не следует изменять DOM (используйте для этого rAF).

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

Вот также статья о 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, всегда используйте пассивные прослушиватели событий, и у вас все будет хорошо.

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

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

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

Продолжить

Ответить на ваш вопрос

  • для отложенной загрузки в бесконечном представлении используйте комбинацию setTimeout + requestIdleCallback для прослушивателей событий и используйте rAF для любой записи макета (мутации DOM).
  • для мгновенных эффектов все еще используйте rAF для любой записи макета (мутации DOM).

Ответ 2

Я также видел несколько упоминаний об использовании requestAnimationFrame, чтобы не только регулировать скорость, но и полностью заменить прослушиватель прокрутки.

Кто-то может прокомментировать это? Я предполагаю, что это плохая практика, потому что он будет продолжать цикл, даже если не прокручивать.