Debouncing TypeScript
Tags: beginners, typescript, javascript, debounce
Published: 2021-07-29 15:06:27

### What is it?

"Debouncing" is a term that means "preventing a function from executing too frequently."

There are instances where a function may be executed way more often than necessary; usually unintentionally. Some instances of this are:

 - The Window `scroll` event
 - The Window `resize` event
 - RequestAnimationFrame
 - SetInterval @ `1000/60` (*60 FPS*)

By utilizing a debouncing technique, rather than executing your complicated code 60 times every second, you could execute it once or twice. You are likely to see noticeable performance improvements by integrating this optimization.

### Example:

JSFiddle: https://jsfiddle.net/76gatsbj/6/


### How does it work?

Debouncing works by comparing timestamps; usually `Date.now()`. It essentially says: "If the last time we ran this function was less than one second ago, then don't run it this time."

Logically, we do this by giving it a threshold of some kind (*500ms, let's say*) then making our time comparison. If the last execution exceeds our threshold + current timestamp, then we execute our function + save the current time.

### Show me code

Here's a simple TypeScript debounce class.

```typescript
/**
 * Debounce
 *
 * Prevents a function from being fired too often by determining
 * a difference in time from the last time in which it was fired
 *
 * @author Matt Kenefick <polymermallard.com>
 */
class Debounce 
{
	/**
	 * Debounced function that we will execute
	 *
	 * @type function
	 */
	public callback: () => void;
	
	/**
	 * Time in between executions
	 *
	 * @type number
	 */
	public threshold: number;

	/**
	 * Last time this function was triggered
	 *
	 * @type number
	 */
	private lastTrigger: number = 0;

	/**
	 * @param function callback
	 * @param number threshold
	 * @return function
	 */
	public constructor(callback: () => void, threshold: number = 200): () => void {
		this.callback = callback;
		this.threshold = threshold;
		
		return this.run.bind(this);
	}
	
	/**
	 * Executable function that applies debounce logic
	 * 
     * @return void
	 */
	public run(): void {
		const now: number = Date.now();
		const diff: number = now - this.lastTrigger;

		if (diff > this.threshold) {
			this.lastTrigger = now;
			this.callback();
		}
	}
}

```

We can implement the debounce class above like so:

```typescript
function myFunction() {
	console.log('This is the debounced function');
}

const event = new Debounce(myFunction, 500);

// Run via interval at 60FPS, execute function every 500ms
setInterval(event, 1000 / 60);
```

Or we can apply it to the scroll event which fires frequently:

```typescript
function myScrollFunction() {
	console.log('This fires on scroll every 200ms');
}

const event = new Debounce(myScrollFunction, 200);

// Run on document scroll, only execute every 200ms
window.addEventListener('scroll', event);
```

### Inclusive vs Exclusive

There are two ways we can approach debouncing techniques: **inclusive** or **exclusive**.

An **inclusive** approach will deny events happening too frequently, but create a single timeout that runs in the future even if the triggering event stops happening. This is usually beneficial for callbacks that have longer thresholds between executions and/or less frequent triggers.

For instance, let's say you have an event you want to fire every 3000ms on scroll. It's very possible the user may stop scrolling in between executions, but you may want the callback to fire one last time. This may be used to re-adjust a viewport that just appeared.

An **exclusive** approach only attempts to execute a callback when the triggers are being applied. In the example above with the 3000ms callback, we would only ever fire **while** the document is being scrolled, but never after it stops.

The code example earlier in the article represents an **exclusive** approach to debouncing.

Here's an example of **inclusive** debouncing: https://jsfiddle.net/719y2fwq/

#### Inclusive Debouncing

```typescript
/**
 * InclusiveDebounce
 *
 * Prevents a function from being fired too often by determining
 * a difference in time from the last time in which it was fired.
 * 
 * Applies inclusive techniques to execute functions one last time.
 *
 * @author Matt Kenefick <polymermallard.com>
 */
class InclusiveDebounce
{
	/**
	 * Debounced function
	 *
	 * @type function
	 */
	public callback: () => void;
	
	/**
	 * Time in between triggers
	 *
	 * @type number
	 */
	public threshold: number;

	/**
	 * Last time this function was triggered
	 *
	 * @type number
	 */
	private lastTrigger: number = 0;

	/**
	 * Timeout for calling future events
	 *
	 * @type number
	 */
	private timeout: number = 0;

	/**
	 * @param function callback
	 * @param number threshold
	 * @return function
	 */
	public constructor(callback: () => void, threshold: number = 200): () => void {
		this.callback = callback;
		this.threshold = threshold;
		
		return this.run.bind(this);
	}
	
	/**
	 * Executable function
	 * 
     * @return void
	 */
	public run(): void {
		const now: number = Date.now();
		const diff: number = now - this.lastTrigger;

        // Execute Immediately
		if (diff > this.threshold) {
			this.lastTrigger = now;
			this.callback();
		}
		
		// Cancel future event, if exists
		if (this.timeout !== 0) {
		    clearTimeout(this.timeout);
			this.timeout = 0;
		}

		// Create future event
		this.timeout = setTimeout(this.callback, this.threshold);
	}
}


// Implementation
// ------------------------------------------------------

function myFunction() {
	console.log('This is an inclusive debounced function');
}

const event = new InclusiveDebounce(myFunction, 1500);

// Test 1: Run on document scroll
window.addEventListener('scroll', event);
```

In the class above, an additional property to store `timeout` was added. The function `run` included additional code to cancel existing timeouts and re-schedule them in the event of an additional execution.

For a more cohesive solution, you could use the logic within the `InclusiveDebounce` class and wrap certain parts with conditionals to have both an **exclusive** or **inclusive** debounce method.

Happy debouncing.