scroll event throttling, requestAnimationFrame, performance optimization

1. scroll event

Because scroll events can fire at high speed, event handlers should not perform computationally expensive operations such as DOM modifications. Instead, it is recommended to use requestAnimationFrame(), setTimeout()or CustomEventto optimize events, as shown below.

  // 使用 requestAnimationFrame 节流
  let lastKnownScrollPosition = 0;
  let ticking = false;

  function doSomething(scrollPosition) {
    
    
    // Do something with the scroll position
  }

  document.addEventListener(
    "scroll",
    function (e) {
    
    
      lastKnownScrollPosition = window.scrollY;

      if (!ticking) {
    
    
        // 使用 requestAnimationFrame 节流
        window.requestAnimationFrame(function () {
    
    
          doSomething(lastKnownScrollPosition);
          ticking = false;
        });

        ticking = true;
      }
    },
    {
    
    
      // 使用 passive 可改善的滚屏性能
      passive: true,
    }
  );

Note that input events and animation frames fire at roughly the same rate, so the above optimization is usually not needed.


2. Use requestAnimationFrame to throttle

1. Screen refresh rate

The speed at which the image is updated on the screen (the number of times the image on the screen appears per second), and its unit is Hertz (Hz).

60Hz: The display is also constantly updating the image on the screen at a frequency of 60 times per second, and the interval between each time is 16.7ms (1000/60≈16.7).

2. Principles of animation

The essence of animation is to let people see the visual effect of the image being refreshed and causing changes. This change must be transitioned in a coherent and smooth manner.

3. setTimeout

The animation realized by using seTimeout may freeze and shake on some low-end machines. reason:

  • The execution time of setTimeout is not deterministic. In Javascript, the setTimeout task is put into an asynchronous queue, and only after the task on the main thread is executed, it will check whether the task in the queue needs to be executed, so the actual execution time of setTimeout is generally longer than its setting later.
  • The refresh rate is affected by the screen resolution and screen size, so the screen refresh rate of different devices may be different, and setTimeout can only set a fixed time interval, which is not necessarily the same as the screen refresh time.

4. requestAnimationFrame

  • requestAnimationFrame makes full use of the refresh mechanism of the display, and the system determines the execution timing of the callback function, thereby saving system resources, improving system performance, and improving visual effects.
  • The pace of requestAnimationFrame follows the refresh pace of the system. It can ensure that the callback function is executed only once in each refresh interval of the screen, so that it will not cause frame loss, nor will it cause the animation to freeze.
  • requestAnimationFrame tells the browser that it wants to perform an animation, and asks the browser to schedule a redraw of the page at the next animation frame.
  • requestAnimationFrame will gather all DOM operations in each frame and complete it in one redraw or reflow, and the time interval of redraw or reflow closely follows the browser's refresh rate.
  • The display has a fixed refresh rate (60Hz or 75Hz), that is to say, it can only redraw 60 or 75 times per second at most. The basic idea of ​​requestAnimationFrame is to keep in sync with this refresh rate and use this refresh rate to redraw the page.
  • Once the page is not in the browser's current tab, it will automatically stop refreshing. This saves CPU, GPU and power.


5. The advantages of requestAnimationFrame

  • CPU energy saving : when the animation implemented by using setTimeout, when the page is hidden or minimized, setTimeout still executes the animation task in the background. Since the page is invisible or unavailable at this time, it is meaningless to refresh the animation, which is a waste of CPU resources. . The requestAnimationFrame is completely different. When the page processing is not activated, the screen refresh task of the page will also be suspended by the system, so the requestAnimationFrame that follows the system will also stop rendering. When the page is activated, the animation will start from the last time. Continue to execute where it stays, effectively saving CPU overhead.
  • Function throttling : In high-frequency events (resize, scroll, etc.), in order to prevent multiple function executions within a refresh interval, use requestAnimationFrame to ensure that the function is only executed once within each refresh interval, which can ensure smoothness performance, and can better save the overhead of function execution. It is meaningless when the function is executed multiple times within a refresh interval, because the display is refreshed every 16.7ms, and multiple drawing will not be reflected on the screen.

6. requestAnimationFrame graceful degradation

Because requestAnimationFrame still has compatibility issues, and different browsers need to carry different prefixes. Therefore, it is necessary to encapsulate requestAnimationFrame in a graceful degradation manner, give priority to the use of advanced features, and then roll back according to the situation of different browsers, until only setTimeout can be used.

// 开源组件库 ngx-tethys里的requestAnimationFrame优雅降级
const availablePrefixes = ['moz', 'ms', 'webkit'];

function requestAnimationFramePolyfill(): typeof requestAnimationFrame {
    
    
    let lastTime = 0;
    return function(callback: FrameRequestCallback): number {
    
    
        const currTime = new Date().getTime();
        const timeToCall = Math.max(0, 16 - (currTime - lastTime));
        const id = setTimeout(() => {
    
    
            callback(currTime + timeToCall);
        }, timeToCall) as any; // setTimeout type warn
        lastTime = currTime + timeToCall;
        return id;
    };
}

function getRequestAnimationFrame(): typeof requestAnimationFrame {
    
    
    if (typeof window === 'undefined') {
    
    
        return () => 0;
    }
    if (window.requestAnimationFrame) {
    
    
        // https://github.com/vuejs/vue/issues/4465
        return window.requestAnimationFrame.bind(window);
    }

    const prefix = availablePrefixes.filter(key => `${
      
      key}RequestAnimationFrame` in window)[0];

    return prefix ? (window as any)[`${
      
      prefix}RequestAnimationFrame`] : requestAnimationFramePolyfill();
}

export function cancelRequestAnimationFrame(id: number): any {
    
    
    if (typeof window === 'undefined') {
    
    
        return null;
    }
    if (window.cancelAnimationFrame) {
    
    
        return window.cancelAnimationFrame(id);
    }
    const prefix = availablePrefixes.filter(
        key => `${
      
      key}CancelAnimationFrame` in window || `${
      
      key}CancelRequestAnimationFrame` in window
    )[0];

    return prefix
        ? ((window as any)[`${
      
      prefix}CancelAnimationFrame`] || (window as any)[`${
      
      prefix}CancelRequestAnimationFrame`])
              // @ts-ignore
              .call(this, id)
        : clearTimeout(id);
}

export const reqAnimFrame = getRequestAnimationFrame();


3. Use passive to improve scrolling performance

Some events on the mobile side, such as touchstart, touchmove, touchend, touchcancel, etc., if the default behavior is blocked in these events, the page will be prohibited from scrolling or zooming.


The browser cannot know in advance whether a listener will prohibit the default behavior, and will not execute the default behavior until the listener is executed. The execution of the listener is time-consuming. If it takes 2 seconds before event.preventDefault(), this will cause the page to freeze.


In order to improve the scrolling experience in this scenario, we need a parameter to tell the browser that there will be no event.preventDefault() in my event listener, and you can scroll as much as you want without waiting for the listener to finish executing. So there is a passive attribute. In order to be compatible with the previous useCapture, the last parameter was changed to an object options.


Therefore, when binding touch and scroll events related to the mobile terminal, use as much as possible { passive: true }to improve performance and experience and avoid page freezes.


However, not all browsers support the passive feature. Browsers that do not support the passive feature will use the last parameter as useCapture, so this subtle code is needed to determine whether the passive feature is supported:

// Test via a getter in the options object to see 
// if the passive property is accessed
var supportsPassive = false;
try {
    
    
  var opts = Object.defineProperty({
    
    }, 'passive', {
    
    
    get: function() {
    
    
      supportsPassive = true;
    }
  });
  window.addEventListener("test", null, opts);
} catch (e) {
    
    }


// Use our detect's results. 
// passive applied if supported, capture will be false either way.
elem.addEventListener(
  'touchstart',
  fn,
  supportsPassive ? {
    
     passive: true } : false
);

By Object.definePropertysetting a passive get accessor and adding a test event, the get accessor will be called when the browser supports it, and supportsPassive is set in the get accessor.


In the Angular framework, Angular CDK has already provided in @angular/cdk/platformthe module normalizePassiveListenerOptions({passive: true})for us to solve compatibility problems. The core code is as follows:

/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

/** Cached result of whether the user's browser supports passive event listeners. */
let supportsPassiveEvents: boolean;

/**
 * Checks whether the user's browser supports passive event listeners.
 * See: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
 */
export function supportsPassiveEventListeners(): boolean {
    
    
  if (supportsPassiveEvents == null && typeof window !== 'undefined') {
    
    
    try {
    
    
      window.addEventListener(
        'test',
        null!,
        Object.defineProperty({
    
    }, 'passive', {
    
    
          get: () => (supportsPassiveEvents = true),
        }),
      );
   } finally {
    
    
      supportsPassiveEvents = supportsPassiveEvents || false;
    }
  }

  return supportsPassiveEvents;
}

/**
 * Normalizes an `AddEventListener` object to something that can be passed
 * to `addEventListener` on any browser, no matter whether it supports the
 * `options` parameter.
 * @param options Object to be normalized.
 */
export function normalizePassiveListenerOptions(
  options: AddEventListenerOptions,
): AddEventListenerOptions | boolean {
    
    
  return supportsPassiveEventListeners() ? options : !!options.capture;
}

Add related Issues for binding events to support passive parameters: https://github.com/angular/angular/issues/8866

Guess you like

Origin blog.csdn.net/Kate_sicheng/article/details/125712876