Debouncing and throttling prevent too many events from being processed. For instance, when a user types in a search bar, you may want to wait until they stop typing for a few milliseconds before executing the search request. This is what debouncing is for. With debouncing, you wait for a period of inactivity before processing an event.
In Blazor, there are two ways to debounce or throttle events: in .NET or in JavaScript. In Blazor WebAssembly, both approaches are roughly equivalent. However, in Blazor Server, every JavaScript event is sent to the server over WebSocket. Filtering events on the server therefore wastes network bandwidth and server CPU. In that case, you should debounce events on the client instead.
#Throttle / Debounce on the server
To throttle events, wrap the event handler method in a throttle or debounce method. Since events are triggered from a timer, you need to use InvokeAsync to switch back to the UI thread, and call StateHasChanged to signal that the component should re-render.
C#
@page "/"
<h2>Throttle</h2>
<input type="text" @oninput="onInputThrottled" />
Value: @value1
<h2>Debounce</h2>
<input type="text" @oninput="onInputDebounced" />
Value: @value2
@code{
string value1;
string value2;
Action<ChangeEventArgs> onInputDebounced;
Action<ChangeEventArgs> onInputThrottled;
protected override void OnInitialized()
{
onInputThrottled = ThrottleEvent<ChangeEventArgs>(e => value1 = (string)e.Value, TimeSpan.FromSeconds(1));
onInputDebounced = DebounceEvent<ChangeEventArgs>(e => value2 = (string)e.Value, TimeSpan.FromSeconds(1));
base.OnInitialized();
}
Action<T> DebounceEvent<T>(Action<T> action, TimeSpan interval)
{
return Debounce<T>(arg =>
{
InvokeAsync(() =>
{
action(arg);
StateHasChanged();
});
}, interval);
}
Action<T> ThrottleEvent<T>(Action<T> action, TimeSpan interval)
{
return Throttle<T>(arg =>
{
InvokeAsync(() =>
{
action(arg);
StateHasChanged();
});
}, interval);
}
// Debounce and Throttle can be moved to another class
Action<T> Debounce<T>(Action<T> action, TimeSpan interval)
{
if (action == null) throw new ArgumentNullException(nameof(action));
var last = 0;
return arg =>
{
var current = System.Threading.Interlocked.Increment(ref last);
Task.Delay(interval).ContinueWith(task =>
{
if (current == last)
{
action(arg);
}
});
};
}
Action<T> Throttle<T>(Action<T> action, TimeSpan interval)
{
if (action == null) throw new ArgumentNullException(nameof(action));
Task task = null;
var l = new object();
T args = default;
return (T arg) =>
{
args = arg;
if (task != null)
return;
lock (l)
{
if (task != null)
return;
task = Task.Delay(interval).ContinueWith(t =>
{
action(args);
task = null;
});
}
};
}
}
#Throttle / Debounce on the client
First, you need JavaScript code to handle events. The idea is to intercept each event and re-dispatch it after the debounce or throttle delay. You can create a file named wwwroot/events.js with the following content:
JavaScript
export function debounceEvent(htmlElement, eventName, delay) {
registerEvent(htmlElement, eventName, delay, debounce);
}
export function throttleEvent(htmlElement, eventName, delay) {
registerEvent(htmlElement, eventName, delay, throttle);
}
function registerEvent(htmlElement, eventName, delay, filterFunction) {
let raisingEvent = false;
let eventHandler = filterFunction(function (e) {
raisingEvent = true;
try {
htmlElement.dispatchEvent(e);
} finally {
raisingEvent = false;
}
}, delay);
htmlElement.addEventListener(eventName, e => {
if (!raisingEvent) {
e.stopImmediatePropagation();
eventHandler(e);
}
});
}
function debounce(func, wait) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, wait);
};
}
function throttle(func, wait) {
var context, args, result;
var timeout = null;
var previous = 0;
var later = function () {
previous = Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
var now = Date.now();
if (!previous) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
Then, create a C# file with two extension methods to call these JavaScript functions:
C#
public static class EventExtensions
{
public static async Task DebounceEvent(this IJSRuntime jsRuntime, ElementReference element, string eventName, TimeSpan delay)
{
await using var module = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./events.js");
await module.InvokeVoidAsync("debounceEvent", element, eventName, (long)delay.TotalMilliseconds);
}
public static async Task ThrottleEvent(this IJSRuntime jsRuntime, ElementReference element, string eventName, TimeSpan delay)
{
await using var module = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./events.js");
await module.InvokeVoidAsync("throttleEvent", element, eventName, (long)delay.TotalMilliseconds);
}
}
Finally, use the extension methods in your Razor components:
Razor
@page "/"
@inject IJSRuntime JSRuntime
<h2>Throttle</h2>
<input @ref="throttledInput" type="text" @bind="value1" @bind:event="oninput" />
Value: @value1
<h2>Debounce</h2>
<input @ref="debouncedInput" type="text" @bind="value2" @bind:event="oninput" />
Value: @value2
@code {
ElementReference throttledInput;
ElementReference debouncedInput;
string value1;
string value2;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.ThrottleEvent(throttledInput, "input", TimeSpan.FromMilliseconds(500));
await JSRuntime.DebounceEvent(debouncedInput, "input", TimeSpan.FromMilliseconds(500));
}
}
}
#Additional resources
Do you have a question or a suggestion about this post? Contact me!