Improve Your React App Performance by Using Throttling and Debouncing

Learn how to improve performance by implementing throttling and debouncing with lodash, underscore or RxJs

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

When building an app using React, we always have this problem of limiting the number of expensive calls, async network requests and DOM updates. We can really check all these using 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 post, we will be looking into how to improve our React app performance by using none of React’s features but rather a general technique applicable not only to React: Throttling and Debouncing.

Tip: Use Bit (GitHub) to easily share your React components into a reusable collection your team can use and develop across projects. Build modular apps faster as a team, save time and make life a bit easier. Give it a try.

Semantic UI components with Bit: Easily turn your components into a reusable collection

Starting with an example

An example will be a good start to explain the goodies the throttling and debouncing brings us.

Let’s say we have an autocomplete component like this:

import React from 'react';import './autocomp.css';class autocomp extends React.Component {
constructor(props) {
super(props);
this.state= {
results: []
}
}
handleInput = evt => {
const value = evt.target.value
fetch(`/api/users`)
.then(res => res.json())
.then(result => this.setState({ results: result.users }))
}
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;

See our autocomplete component, once we enter a word in the input box it requests the api/users to fetch a list of users to display. On every letter type, the async network request is triggered and on success, the DOM is updated with the result via the this.setState call.

Now, imagine typing fidudusola and we are trying to search for fidudusolanke, there will be many names that go with fidudusola so we will type fidudusola to narrow the searches down.

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

This name has 10 letters so doing the math, we will have 10 API requests and 10 DOM updates. Just for a single user!! To eventually get to see our intended name fidudusolanke appear with fewer results.

Even if the autocomplete lookups can be done without network requests (e.g. you have a local “database” in-memory) there’s still expensive DOM updates for that needs to be done for every single character/word typed in.

const data = [
{
name: 'nnamdi'
},
{
name: 'fidudusola'
},
{
name: 'fashola'
},
{
name: 'fidudusolanke'
},
// ... up to 10,000 records
]
class autocomp extends React.Component {
constructor(props) {
super(props);
this.state= {
results: []
}
}
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.handleInput} />
<div>
{results.map(result=>{result})}
</div>
</div>
);
}
}

Another example is the use and attachment of events like resize and scroll. Mostly, a website is scrolled like a 10 times per second. Imagine attaching an event handler to the scroll event

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

You will see your function being fired 10 times per f***** second!!!
You’ll face the worst if your event handler does a heavy computation or heavy DOM manipulations.

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

See, we have an operation that will take 9 seconds to complete until we see Scrolled !!!. If we scroll the body element to 5000px we will see 200+ events being fired. So, it will take our event handler 9 secs to finish one event run and approx. 9 * 200 = 1800s to run the whole 200 events. Therefore, it will take 30 mins(half an hour) for the events to run finish.

You will surely notice a lag and unresponsive browser. it’s best to write your event handler code to execute in lesser time.

So we see that this will have a huge performance bottleneck in our app. We don’t really need to perform API request and DOM updates on each letter typed. We need to wait until the user either stop typing or types for a considerable amount of time. We need to wait until the user scrolls for an amount of time or stops scrolling.

All these ensure good performance in our app.

Let’s see how we can use throttling and debouncing to avoid this performance bottleneck.

Throttling

Throttling enforces a maximum number of times a function can be called over time. As in “execute this function at most once every 100 milliseconds.”

Throttling executes a given function after a specified amount of time has elapsed. This limits the number of times a function is called, this is reasonable when the repeated function calls won’t reset any data.

Let’s say we call a function normally on the rate of 1000 times/ 20 secs. If we throttle it to execute in every 500ms, we would see that in 20secs it would execute the function in 40 times / 20 secs:

1000 * 20 secs = 20,000ms
20,000ms / 500ms = 40 times

This is hugely optimized from 1000 to 40.

To throttle in React, we will use the underscore and lodash libraries, RxJS and our own implementation.

Using underscore

We will use the underscore library to throttle our autocomp component. Underscore library has a throttle function that does throttling.

We will install the underscore library:

npm i underscore

Then import it in our component file:

// ...
import * as _ from underscore;
class autocomp extends React.Component {
constructor(props) {
super(props);
this.state = {
results: []
}
this.handleInputThrottled = _.throttle(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>
);
}
}

The throttle function is passed a function that wants to be throttled and the amount of time it is to be throttled, it returns a function that is throttled. Here in our case, the handleInput method is passed to the throttle function and we want it to be throttled to 100ms.

Now, if we type fidudusola at a normal speed of 1 letter per 200ms. In our throttled comp it would take 10 * 200ms = (2000ms) 2s to type fidudusola that is the handleInput method would be called 2 (2000ms/ 100ms = 2) times instead of 10 times initially in our non-throttled autocomp component.

See our component would have to update twice for typing fidudusola it is appreciable for the 10-letter word, other than the 10 re-renders for the initial non-throttled component.

Using lodash

The lodash library also provides a throttling function to enables us to use it in our JS apps.

First, we need to install the library:

npm i lodash

(You can also use the Lodash collection on Bit)

Using lodash, our autocomp would be like this:

// ...
import { throttle } from lodash;
class autocomp extends React.Component {
constructor(props) {
super(props);
this.state = {
results: []
}
this.handleInputThrottled = throttle(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>
);
}
}

Still the same as underscore, nothing changed.

Using RxJS

The Reactive Extensions in JS provided us with an operator we can use to achieve throttling in JS.

First, we install the rxjs library:

npm i rxjs

We import throttle operator from the rxjs library:

// ...
import { BehaviorSubject } from 'rxjs';
import { throttle } from 'rxjs/operators';
class autocomp extends React.Component {
constructor(props) {
super(props);
this.state = {
results: []
}
this.inputStream = new BehaviorSubject()
}
componentDidMount() {
this.inputStream
.pipe(
throttle(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>
);
}
}

We imported throttle and BehaviorSubject from the rxjs library. We initialized a BehaviorSubject instance to be held in the inputStream property. In the componentDidMount hook we piped the inputStream stream to the throttle operator, we passed in 100 value, this tells RxJS to throttle the stream from the inputStream for 100ms. The stream returned by the throttle operator is subscribed to so as to get the stream value.

Looking at the input box when we start typing the value is emitted to the inputStream stream. Since we are subscribed to the inputStream when the component mounted. First, no value is emitted to the stream after the elapse of 100ms because of the throttle operator, then the latest value is emitted. Once emitted we run the search to get the autocomplete results.

If we type fidudusola at a speed of 1 letter per 200ms. The component will be re-rendered 200ms / 100ms = 2 times.

using our own custom implementation

To better understand how throttling it is best we create our own throttling implementation.

We understand that in a throttled function it is called at the specified interval, to achieve that we will use the setTimeout function.

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();
}
}

The above implementation is very easy. We used the setTimeout API to schedule the time interval the passed in function fn will be executed. Plugging it in in our React app:

// ...
class autocomp extends React.Component {
constructor(props) {
super(props);
this.state = {
results: []
}
this.handleInputThrottled = throttle(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>
);
}
}

Debouncing

Debouncing enforces that a function will not be called again until a certain amount of time has passed since its last call. As in “execute this function only if an amount of time (ex. 100 milliseconds) have passed without it being called.”

In debouncing, it ignores all calls to a function and waits until the function has stopped being called for a specified amount of time.

Let’s make our auto component use debouncing:

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>
);
}
}

using 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>
);
}
}

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>
);
}
}

Importance of Throttling and Debouncing: Gaming

There are so many instances where throttling and debouncing are very much needed. The most needed area is in gaming, the most performed action in gaming is key-pressing either in computer keyboard or in gamepad. Actions like shooting, accelerating, the gamer might press a key often times (40 times per 20 secs ie 2 times a sec) but no matter how times the gamer presses the shooting key it will only fire once (let’s say per sec). It will be best to throttle it to 1 second so the second button press will be ignored.

Gaming is majorly the area in which throttling and debouncing are heavily used.

Conclusion

We saw how throttling and debouncing improves the performance of our React apps. Instead of repeatedly calling methods in React apps actually affects the performance because there will be un-necessary re-rendering of the component and its subtree. It might be un-noticed in small apps but the effects will be noticeable in large-scale apps.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.

Thanks !!!

Learn More

Credits

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕