Update: Thank you for your support. Recently, I have made a blog official website come out to facilitate your systematic reading. There will be more content and more optimization in the follow-up

—— The following is the text ——

译 文 : Improve Your React App Performance by Using Throttling and Debouncing

The introduction

When building applications with React, there are always some limitations we encounter, such as a large number of calls, asynchronous network requests, and DOM updates, which we can check with the features provided by React.

  • shouldComponentUpdate(...)Lifecycle hook
  • React.PureComponent
  • React.memo
  • Windowing and Virtualization
  • Memoization
  • Hydration
  • Hooks (useState.useMemo.useContext.useReducer, etc.)

In this article, we’ll look at how to improve React application performance without using the features provided by React, and we’ll use a technology that doesn’t just apply to React: throttling and Debounce.

Let’s start with an example

Example 1

The following example illustrates the benefits of throttling and damping. Suppose we have an Autocomp component

import React from 'react';
import './autocomp.css';
Copy the code
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state= {
            results: []}}Copy the code
    handleInput = evt= > {
        const value = evt.target.value
        fetch(`/api/users`)
            .then(res= > res.json())
            .then(result= > this.setState({ results: result.users }))
    }
Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
            <input placeholder="Enter your search.." onChange={this.handleInput} />
            <div>
                {results.map(item=>{item})}
            </div>
            </div>
        );
    }
}
export default autocomp;
Copy the code

In our Autocomp component, once we type a word into the input box, it asks API/Users for a list of users to display. After each letter is entered, an asynchronous network request is triggered and the DOM is updated with this.setState upon success.

Now, imagine typing in fidudusola and trying to find the result fidudusolanke. There will be many names that appear with fidudusola.

1.  f
2.  fi
3.  fid
4.  fidu
5.  fidud
6.  fidudu
7.  fidudus
8.  fiduduso
9.  fidudusol
10. fidudusola
Copy the code

The name has 10 letters, so we’ll have 10 API requests and 10 DOM updates, and that’s just one user!! Finally, the expected name fidudusolanke appears with the rest of the results.

Even though Autocomp can do this without a network request (for example, with a local “database” in memory), it still requires an expensive DOM update for every character/word entered.

const data = [
    {
        name: 'nnamdi'
    },
    {
        name: 'fidudusola'
    },
    {
        name: 'fashola'
    },
    {
        name: 'fidudusolanke'
    },
    / /... Up to 10000 records
]
Copy the code
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state= {
            results: []}}Copy the code
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInput} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

Example 2

Another example is using events such as resize and Scroll. Most of the time, websites scroll 1,000 times per second. Imagine adding an event handler to the Scroll event.

document.body.addEventListener('scroll', () = > {console.log('Scrolled !!! ')})Copy the code

You will find that this function is executed 1000 times per second! If this event handler performs a lot of computation or a lot of DOM manipulation, the worst case scenario is possible.

function longOp(ms) {
    var now = Date.now()
    var end = now + ms
    while(now < end) {
        now = Date.now()
    }
}
Copy the code
document.body.addEventListener('scroll', () = > {// simulating a heavy operation
    longOp(9000)
    console.log('Scrolled !!! ')})Copy the code

We have an operation that takes 9 seconds to complete and finally outputs Scrolled!! Suppose we scroll 5000 pixels and 200 more events are triggered. Therefore, an event that takes 9 seconds to complete will take approximately 9 * 200 = 1800s to run all 200 events. Therefore, it takes 30 minutes (half an hour) to complete.

So you’re bound to find a lagged and unresponsive browser, so it’s better to write an event handler that executes in a short amount of time.

We found it in our great performance bottlenecks in the application, we don’t need the input of each letter on API request and update the DOM, we need to wait until the user stops input or input after a period of time, wait until the user stops rolling or rolling after a period of time, to implement the event handler.

With all this in mind to ensure good performance for our application, let’s take a look at how to avoid this performance bottleneck using throttling and damping.

Throttling Throttle

Throttling forces the maximum number of times a function can be called over a period of time, such as at most once every 100 milliseconds.

Throttling is the execution of a given function once for a specified amount of time. This limits the number of times a function can be called, so repeated function calls do not reset any data.

Let’s say we normally call a function at a rate of 1000 times per 20 seconds. If we use throttling to limit it to one execution every 500 milliseconds, we see that the function executes 40 times in 20 seconds.

1000 * 20 secs = 20.000ms
20.000ms / 500ms = 40 times
Copy the code

This is a huge optimization from 1000 to 40.

Here’s an example of using throttling in React, using underscore, Lodash, RxJS, and custom implementations, respectively.

Use the underscore

We’ll use the throttling functions provided by underscore to handle our AutoComp component.

Install the dependencies first.

npm i underscore
Copy the code

Then import it into the component:

// ...
import * as _ from underscore;
Copy the code
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.handleInputThrottled = _.throttle(this.handleInput, 1000)}Copy the code
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInputThrottled} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

The throttling function takes two arguments, the function to be restricted and the time difference, and returns a function that has been throttled. In our example, the handleInput method is passed to the Throttle function with a time difference of 1000ms.

Now, assuming we enter fidudusola at the normal rate of 1 letter per 200ms, it takes 10 * 200ms = (2000ms) 2s to complete the input, Then the handleInput method will only be called 2 (2000ms / 1000ms = 2) times instead of the original 10 times.

Using lodash

Lodash also provides a throttle function that we can use in JS programs.

First, we need to install the dependencies.

npm i lodash
Copy the code

With Lodash, our Autocomp would look like this.

// ...
import { throttle } from lodash;
Copy the code
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.handleInputThrottled = throttle(this.handleInput, 100)}Copy the code
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInputThrottled} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

Same effect as underscore, no other difference.

Using RxJS

The Reactive Extensions in JS provide a throttling operator that you can use to implement functionality.

First, we install RXJS.

npm i rxjs
Copy the code

We import throttle from the RXJS library

// ...
import { BehaviorSubject } from 'rxjs';
import { throttle } from 'rxjs/operators';
Copy the code
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.inputStream = new BehaviorSubject()
    }
Copy the code
    componentDidMount() {
        this.inputStream
            .pipe(
                throttle(1000)
            )
            .subscribe(v= > {
                const filteredRes = data.filter((item) = > {
                    // algorithm to search through the `data` array
                })
                this.setState({ results: filteredRes })
        })
    }

Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={e= > this.inputStream.next(e.target.value)} />
                <div>
                    {results.map(result => { result })}
                </div>
            </div>); }}Copy the code

We import throttle and BehaviorSubject from RXJS, initialize a BehaviorSubject instance and save it in the inputStream property in componentDidMount, We pass the inputStream to the throttling operator, passing 1000 to indicate that the RxJS throttling control is 1000ms, and the stream returned by the throttling operation is subscribed to get the stream value.

Because the component is subscribed to the inputStream when it is loaded, the input is sent to the inputStream when we start typing. At first, since the throttle operator does not send anything for 1000ms, the latest value is sent after that, and then the calculation begins to get the results.

If we enter fidudusola at 200ms 1 letter, the component will re-render 2000ms / 1000ms = 2 times.

Use a custom implementation

We implemented our own throttling function to better understand how throttling works.

We know that in a throttle-controlled function, it will be called at a specified interval, and we will use the setTimeout function to do this.

function throttle(fn, ms) {
    let timeout
    function exec() {
        fn.apply()
    }
    function clear() {
        timeout == undefined ? null : clearTimeout(timeout)
    }
    if(fn ! = =undefined&& ms ! = =undefined) {
        timeout = setTimeout(exec, ms)
    } else {
        console.error('callback function and the timeout must be supplied')}// API to clear the timeout
    throttle.clearTimeout = function() { clear(); }}Copy the code

Note: the original custom implementation of the throttling function is a problem, the detailed implementation and analysis of the throttling function can be seen in another article, click to view

My implementation is as follows:

// fn is the function to execute
// Wait is the time interval
const throttle = (fn, wait = 50) = > {
  // The last time fn was executed
  let previous = 0
  // Return throttle as a function
  return function(. args) {
    // Get the current time and convert it to a timestamp in milliseconds
    let now = +new Date(a)// Compare the current time to the last time the function was executed
    // If the wait time is longer, set previous to the current time and execute fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}
Copy the code

The above implementation is very simple and will be used in the React project as follows.

// ...
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.handleInputThrottled = throttle(this.handleInput, 100)}Copy the code
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
Copy the code
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInputThrottled} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

Stabilization Debounce

Shake stabilization forces the function to be called again only after a certain amount of time has elapsed since the last call, such that the function is executed only after a certain amount of time (such as 100 milliseconds) has elapsed if it has not been called.

In stabilization, it ignores all calls to a function until the function stops calling for some time.

An example of using Debounce in a project is shown below.

Use the underscore

// ...
import * as _ from 'underscore';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.handleInputThrottled = _.debounce(this.handleInput, 100)
    }
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInputThrottled} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

Using lodash

// ...
import { debounce } from 'lodash';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.handleInputThrottled = debounce(this.handleInput, 100)
    }
    handleInput = evt= > {
        const value = evt.target.value
        const filteredRes = data.filter((item) = > {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={this.handleInputThrottled} />
                <div>
                    {results.map(result=>{result})}
                </div>
            </div>); }}Copy the code

Using RxJS

// ...
import { BehaviorSubject } from 'rxjs';
import { debounce } from 'rxjs/operators';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []}this.inputStream = new BehaviorSubject()
    }
    componentDidMount() {
        this.inputStream
            .pipe(
                debounce(100)
            )
            .subscribe(v= > {
                const filteredRes = data.filter((item) = > {
                    // algorithm to search through the `data` array
                })
                this.setState({ results: filteredRes })
        })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'>
                <input placeholder="Enter your search.." onChange={e= > this.inputStream.next(e.target.value)} />
                <div>
                    {results.map(result => { result })}
                </div>
            </div>); }}Copy the code

Important area: Games

There are a number of situations where throttling and stabilization are needed, and one of the areas where they are most needed is in gaming. The most common action in games is to press a button on a computer keyboard or gamepad. The player may often press the same key multiple times (40 times every 20 seconds, or 2 times per second) such as shoot, speed up, but no matter how many times the player presses the shoot key, it will only fire once (say per second). So use the throttle control for 1 second so that the second button press is ignored.

conclusion

We saw how throttling and shaking can improve the performance of a React application, and how repeated calls can affect performance because components and their subtrees will be unnecessarily rerendered, so repeated calls to methods in a React application should be avoided. It’s not noticeable in a small program, but it can be noticeable in a large program.

❤️ see three things

If you found this post inspiring, I’d like to invite you to do three small favors for me:

  1. Like, let more people can also see this content (collection does not like, is playing rogue – -)
  2. Follow me on GitHub and make it a long-term relationship
  3. Pay attention to the public number “advanced front-end advanced”, focus on solving a front-end interview difficult points every week, the public number background reply “data” to send you selected front-end quality data.