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

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

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

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

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

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

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

4b9b3361

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

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

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

requestAnimationFrame

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

с помощью rAF для дроссельной заслонки /debounce

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

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):

/**
 * @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);
    };
}

reqeuestIdleCallback

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

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

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

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

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

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

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

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

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

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

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

Резюме

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

  • для lazyloading, бесконечный вид использует комбинацию setTimeout + requestIdleCallback для ваших прослушивателей событий и используйте rAF для любой записи макета (DOM-мутации).
  • для мгновенных эффектов по-прежнему используют rAF для любой компоновки (DOM-мутации).
16
ответ дан 27 июня '17 в 14:36
источник