Table of Contents
In this article we'll talk about Debouncing and Throttling, two optimization techniques that we might want to consider whenever we're developing an Angular app that needs features such as autocomplete, real-time filtered searches, and so on: learning how to efficiently use both of them will not only improve our overall knowledge of the Angular (and RxJS) codebase, but will also ensure important performance improvements because it will greatly reduce the number of HTTP requests that our client-side code will issue to the server to retrieve data.
Introduction
Let's take the following Angular code snippet:
1 2 3 4 5 6 7 8 9 10 11 12 |
loadData(filterQuery: string = null) { var url = this.baseUrl + 'api/MyData'; if (filterQuery) { params = new HttpParams() .set("filterQuery", this.filterQuery); } this.http.get<any>(url, { params }) .subscribe(result => { this.jsonData = result.jsonData; }, error => console.error(error)); } |
As we can see, this is a typical data retrieval method that will fetch a jsonData result from the server using a standard HTTP request using a local http object (most likely an instance of HttpClient instantiated in the constructor using Dependency Injection ) and store it within a local variable; such approach is tipically used by most Angular components (and services) that perform such kind of tasks.
The optional filterQuery parameter tells us that such method will be likely called from an input HTML element that acquires some user-defined value, such as the following one:
1 |
<input (keyup)="loadData($event.target.value)" placeholder="Filter results..."> |
To put it in other words, we can say that we're dealing with a real-time filter that works in the following way:
- the user writes some stuff within the input HTML element;
- our Angular app sends such value (filterQuery) to the server;
- the server performs a query to the data source (ideally a database), using the filterQuery to filter out the results, and returns a JSON object containing the results.
The filter works in real-time because the call to the loadData method is directly bound to the HTML input's keyup event, meaning that will fire upon each user's keystroke. That's great in terms of user experience, because our users will immediately get filtered data as they type.
However, this real-time filter has a serious downside in terms of performance impact: every time the filter text changes (i.e. upon each keystroke), Angular fires an HTTP request to the back-end to retrieve the updated list of results. Such behavior is intrinsically resource-intensive and can easily become a huge performance issue, especially if we’re dealing with large tables and/or non-indexed columns.
Are there ways to improve this approach without compromising the results obtained in terms of user experience? As a matter of fact, the answer is yes, as long as we’re willing to implement a couple widely used techniques specifically meant to improve the performance of code that gets executed repeatedly within a short period of time: throttling and debouncing.
Throttling and Debouncing
To better understand the concepts of throttling and debouncing, let's try to make a real-life example.
If we think about it, our everyday life is full of situations where we are forced to do something while our attention is captured by something else: social networks like Twitter and instant messaging apps such as WhatsApp are a perfect example of that, since they literally flood us with notifications regardless of what we’re doing.
What do we usually do in these cases? Let’s consider the following alternatives:
- Respond to all notifications in real time, which would be great for the requesting party but would compromise what we're doing.
- Take no immediate action and check our messages only once every, let’s say, five minutes.
- Take no immediate action and check our messages only when no new notifications have come in for the last five minutes.
The first approach is what our sample code is currently doing; the second is called throttling, while the third is called debouncing.
Let’s try to better understand what these terms actually mean.
Definitions
In software development, throttling is used to define a behavior that enforces a maximum number of times a function can be called over time.
To put it in other words, it’s a way to say:
Let’s execute this function at most once every N milliseconds.
No matter how many times the user fires the event, that function will be executed only once in a given time interval.
The term debouncing is used to define a technique that prevents a function from being called until a certain amount of time has passed without it being called.
In other words, it’s a way to say:
Let’s execute this function only if N milliseconds have passed without it being called.
The concept has some similarities with to throttling technique, with an important difference: no matter how many times the user fires the event, the attached function will be executed only after the specified time once the user stops firing the event.
In a nutshell, we could say that the main difference between throttling and debouncing is that throttling executes the function at a regular (given) interval, while debouncing executes the function only after a (given) cooling period.
Why should we do that?
Before moving forward, let's try to answer to a simple question: why should we implement throttling and/or debouncing in the first place?
Let’s cut it short: in information technology, throttling and debouncing are mostly useful for two main reasons: optimization and performance. They are widely used in JavaScript because they can be very helpful to efficiently handle some resource intensive DOM-related tasks, such as scrolling and resizing HTML components, as well as retrieving data from the server.
In our given scenario, we can think of them as two ways to optimize event handling, thus lifting some work from our server (controller and database): more specifically, we want to find a way to reduce the HTTP requests that Angular currently makes to our server upon each keystroke.
Shall we do that using throttling or debouncing?
If we think about how the filter function works in terms of user experience, we can easily find the correct answer. Since we’re talking about a textbox that can be used to filter the listing results to those who contain one or more characters typed by the user, we can reasonably conclude that we could defer the HTTP request until the user stops typing, as long as we process it right after it does. Such behaviour won’t hinder the user experience granted by the current filter while preventing a good number of unnecessary HTTP calls.
In other words, we need to debounce our calls to the back-end: let’s see how we can do that.
Debouncing calls to the back-end
An easy approach to debounce with Angular is given by RxJS, the Reactive Extensions for JavaScript library that allows us to use Observables to perform our HTTP calls.
Since the Angular HttpClient already uses Observables to handle its HTTP requests, we’re halfway done: we just need to make use of the handy debounceTime RxJS operator, which will emit a value from the source Observable only after a particular time span has passed without another source emission.
While we are there, we can also take the chance to add the distinctUntilChanged operator as well, which emits a value only if it’s different from the last one inserted by the user: this will prevent any HTTP call identical to the previous one, which could happen – for example – if the user writes a sentence, then adds a letter and immediately deletes it.
Implementing Debouncing with debounceTime
To implement such logic we need to perform the following changes to the above sample code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import { Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; filterTextChanged: Subject<string> = new Subject<string>(); // debounce filter text changes onFilterTextChanged(filterText: string) { if (this.filterTextChanged.observers.length === 0) { this.filterTextChanged .pipe(debounceTime(1000), distinctUntilChanged()) .subscribe(filterQuery => { this.loadData(filterQuery); }); } this.filterTextChanged.next(filterText); } loadData(filterQuery: string = null) { var url = this.baseUrl + 'api/MyData'; if (filterQuery) { params = new HttpParams() .set("filterQuery", this.filterQuery); } this.http.get<any>(url, { params }) .subscribe(result => { this.jsonData = result.jsonData; }, error => console.error(error)); } |
As we can see, we didn’t touch the loadData method at all, so that we won’t mess the existing code: we added a new onFilterTextChanged method instead, which will be called by the filter’s input and will transparently handle the debouncing task.
If we take a closer look to the onFilterTextChanged method we can see that it works with a new filterTextChanged variable that we’ve also added to our component class: such variable hosts a Subject, a special type of Observable that allows values to be multi-casted to many Observers.
In a nutshell, here’s what this new method does every time it gets called by the filter’s input method:
- Checks the filterTextChangedSubject to see if there are Observers listening; if there are no Observers yet, pipes the debounceTime and distinctUntilChanged operators and adds a new subscription for the loadData
- Feeds a new value to the Subject, which will be multi-casted to the Observers registered to listen to it.
Although we’ve already explained what these operators do, let’s quickly recap their role again:
- debounceTime will emit the value after 1000 milliseconds of no source input coming from the user;
- distinctUntilChanged will emit the value only if it’s different than the last inserted one.
Now that we’ve implemented the debouncing logic in the Angular class, we just need to update the component’s template file to make the filter’s input call the new onFilterTextChanged method instead of loadData.
1 |
<input (keyup)="onFilterTextChanged($event.target.value)" placeholder="Filter results..."> |
That’s it: delaying these HTTP requests in these two components will shut out most unnecessary HTTP requests coming from our Angular app, thus preventing our database from being called over and over rapidly.
Implementing Throttling with throttleTime
In our given scenario, debouncing is definitely the way to go because it allows to optimize the HTTP calls issued by our real-time filter while still granting an almost-immediate user experience; that said, we can definitely spend a couple more minutes to see how we could implement throttling instead.
Luckily enough, the throttling technique can be implemented using the same approach that we’ve used for debouncing: all we need to do is replacing the debounceTime RxJS operator with throttleTime.
Here's the sample code for throttling:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import { Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; filterTextChanged: Subject<string> = new Subject<string>(); // debounce filter text changes onFilterTextChanged(filterText: string) { if (this.filterTextChanged.observers.length === 0) { this.filterTextChanged .pipe(throttleTime(2000)) .subscribe(filterQuery => { this.loadData(filterQuery); }); } this.filterTextChanged.next(filterText); } loadData(filterQuery: string = null) { var url = this.baseUrl + 'api/MyData'; if (filterQuery) { params = new HttpParams() .set("filterQuery", this.filterQuery); } this.http.get<any>(url, { params }) .subscribe(result => { this.jsonData = result.jsonData; }, error => console.error(error)); } |
As we can see, the above code will limit the numbers of HTTP requests issued by our client to 1 every two seconds (2000 ms): as we've clearly said early on, such approach is clearly less efficient in our given scenario, because it doesn't take any advantage from the user pauses like the debouncing approach did.
Useful links
For additional info regarding the debounceTime and throttleTime RxJS operators, refer to the following pages from the RxJS official guide:
- https://rxjs-dev.firebaseapp.com/api/operators/debounceTime
- https://rxjs-dev.firebaseapp.com/api/operators/throttleTime
Conclusions
That's it, at least for now: we hope that such guide to debouncing and throttling, as well as the provided code samples, will help other Angular developers to fully understand these important concept and use them to further optimize their code.