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

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

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

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

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

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

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

1 ответов


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

в общем все ваши запрошенные инструменты (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. Почему это помогает? The rAF убеждается что вы двигаете ваше invalidation плана (на ende рамки). Это означает любой другой код, который вызывается внутри того же фрейма работает на допустимом макете и может работать с супер быстрыми методами чтения макета.

этот шаблон на самом деле настолько велик, что я бы предложил следующий вспомогательный метод (написанный в 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 обратные вызовы вызываются, вы не должны изменять DOM (используйте rAF для этого).

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

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

пассивный событие слушатель!--61-->

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

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

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

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

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

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

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

резюме

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

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