Github Code Repository
In the fast paced world, most of the jobs we do are on the Web, and fast. Creating a seamless, smooth user experience becomes all the more important. Consumers love a UI which works fast, without lags or delays. Achieving a near perfect experience is possible, although tricky. Have you heard of Event Loops?
In JavaScript, Event loop is a fundamental concept that manages the order of execution of code, collects processes, puts instructions in queued sub-tasks and runs asynchronous operations efficiently. Here's a quick breakdown of how an event loop works:
This event loop continuously checks the call stack. Execution of a JavaScript code continues until the call stack is empty.
Event handling is a very crucial part of building JavaScript applications. In such an application we may need to associate multiple events with an UI component.
You have a Button in a UI which helps populate a table with the latest news in Sports. Now this requires you to:
These 3 processes are chained together in a synchronous manner. Now repeatedly pressing on the Button would mean multiple API calls - resulting in the UI being blocked for quite a couple of seconds - a seemingly laggy User Experience.
This is a good use case for approaches like Debouncing and Throttling. For events like this, that trigger a chain of complex events - we can use such maneuvers to limit the number of times we are calling the API, or in a general sense - limit the rate at which we process an event.
Debouncing: Deferring the execution of a function until a specified cooldown period has elapsed since the last event.
For example:
If we debounce handleOnPressKey() for 2 seconds, it will execute only if the user stops pressing keys for 2 seconds.
Code Snippet:
let debounceTimer; // Timer reference const handleOnPressKey = () => { console.log("Key pressed and debounce period elapsed!"); }; const debouncedKeyPress = () => { // Clear any existing timer clearTimeout(debounceTimer); // Start a new debounce timer debounceTimer = setTimeout(() => { handleOnPressKey(); // Execute the function after cooldown }, 2000); // Cooldown period of 2000ms }; // Attach debouncedKeyPress to keypress events document.getElementById("input").addEventListener("keypress", debouncedKeyPress);
Throttling: Ensuring a function is called at most once within a specified time period, regardless of how often the event occurs.
For example:
If we throttle handleOnScroll() with a 2-second interval, the function will execute at most once every 2 seconds, even if the scroll event triggers multiple times within that period.
Code Example:
let throttleTimer; // Timer reference const handleOnScroll = () => { console.log("Scroll event processed!"); }; const throttledScroll = () => { if (!throttleTimer) { handleOnScroll(); // Execute the function immediately throttleTimer = setTimeout(() => { throttleTimer = null; // Reset timer after cooldown }, 2000); // Cooldown period of 2000ms } }; // Attach throttledScroll to scroll events document.addEventListener("scroll", throttledScroll);
Let's quickly take a look at the HTML code before jumping on the more critical script.js
We have used TailwindCSS for quick styling. You can check their documentation here Tailwind Documentation - it's massively helpful for making quick prototypes
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Event Loop Practice</title> <!-- Tailwind CSS CDN --> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <style> /* Tailwind Extensions (Optional for Customizations) */ body { font-family: 'Inter', sans-serif; } </style> </head> <body> <h3> Why Use Fuse.js? </h3> <p>Fuse.js is a lightweight, customizable library for fuzzy searching. It handles typos and partial matches, offers high performance for large datasets, and has an intuitive API. This will help enhance your search functionality with flexible, user-friendly search experiences. Additionally, this provides you with a CDN link, so it can work right of the bat, no imports or local storage required.</p> <h2>Now let's Code in the Real Deal - The JS</h2> <h4> 1. Task Array and Variables </h4> <pre class="brush:php;toolbar:false">const tasks = new Array ( "Complete Blog on Throttling + Debouncing", "Make a list of 2025 Resolutions", ); let fuse = undefined; let debounceTimer; let throttleTimer;
This section initializes an array of tasks and declares variables for Fuse.js, debounce timer, and throttle timer. We have hardcoded some tasks already - for the sake of this project
Now let's build the onSubmit function. This function will be triggered once the user clicks on the Submit Arrow. It prevents the default form submission, retrieves the input value, clears the input field, adds the new task to the tasks array, and updates the task list.
let debounceTimer; // Timer reference const handleOnPressKey = () => { console.log("Key pressed and debounce period elapsed!"); }; const debouncedKeyPress = () => { // Clear any existing timer clearTimeout(debounceTimer); // Start a new debounce timer debounceTimer = setTimeout(() => { handleOnPressKey(); // Execute the function after cooldown }, 2000); // Cooldown period of 2000ms }; // Attach debouncedKeyPress to keypress events document.getElementById("input").addEventListener("keypress", debouncedKeyPress);
Now we need to ensure that once a task has been submitted, it gets updated in the Task list
let throttleTimer; // Timer reference const handleOnScroll = () => { console.log("Scroll event processed!"); }; const throttledScroll = () => { if (!throttleTimer) { handleOnScroll(); // Execute the function immediately throttleTimer = setTimeout(() => { throttleTimer = null; // Reset timer after cooldown }, 2000); // Cooldown period of 2000ms } }; // Attach throttledScroll to scroll events document.addEventListener("scroll", throttledScroll);
The updateList() function renders the task list by looping through the tasks array and creating list items for each task. Each list item includes a bullet point and the task text.
Now we need to ensure that the list gets update after Page is loaded, the first time. We also would want to initialize Fuse.js on Page load - and associate the tasks array with it. Remember, we would want to render suggestions from this tasks array within the dropdown.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Event Loop Practice</title> <!-- Tailwind CSS CDN --> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <style> /* Tailwind Extensions (Optional for Customizations) */ body { font-family: 'Inter', sans-serif; } </style> </head> <body> <h3> Why Use Fuse.js? </h3> <p>Fuse.js is a lightweight, customizable library for fuzzy searching. It handles typos and partial matches, offers high performance for large datasets, and has an intuitive API. This will help enhance your search functionality with flexible, user-friendly search experiences. Additionally, this provides you with a CDN link, so it can work right of the bat, no imports or local storage required.</p> <h2>Now let's Code in the Real Deal - The JS</h2> <h4> 1. Task Array and Variables </h4> <pre class="brush:php;toolbar:false">const tasks = new Array ( "Complete Blog on Throttling + Debouncing", "Make a list of 2025 Resolutions", ); let fuse = undefined; let debounceTimer; let throttleTimer;
const onSubmit = (event) => { //Prevent default event.preventDefault(); const text = document.getElementById("input").value.trim(); document.getElementById("input").value = ""; tasks.push(text); updateList(); }
Now we need to ensure that on every 'input' we search through the list to show suggestions in the dropdown. This has 3 parts:
const updateList = () => { const lists = document.getElementById("taskList"); lists.innerHTML = ""; //Loop through all elements in tasks tasks.forEach(task => { const taskElement = document.createElement("li"); taskElement.classList.add("flex", "items-center", "space-x-2"); //Add Bullet Point Element const bullet = document.createElement("span"); bullet.classList.add("h-2", "w-2", "bg-blue-500", "rounded-full"); //Add Span Tag const taskText = document.createElement("span"); taskText.textContent = task; taskElement.appendChild(bullet); taskElement.appendChild(taskText); lists.appendChild(taskElement); }) }
const init = () => { console.log("Initializing..."); //Update and render the list updateList(); //Initialize Fuse with the updated array try{ fuse = new Fuse(tasks, { includeScore: true, threshold: 0.3 //For sensitivity }) } catch(e) { console.log("Error initializing Fuse:"+ fuse); } }
document.addEventListener("DOMContentLoaded", init);
So far: The dropdown list will update everytime you type something - in a more bulky UI we would not want this experience
Updating the dropdown list on every keystroke in a bulky UI can lead to performance issues, causing lag and a poor user experience. Frequent updates can overwhelm the event loop, leading to delays in processing other tasks.
We will now see how we can use Debouncing OR throttling to help manage the frequency of updates, ensuring smoother performance and a more responsive interface.
Here's how we can implement either of the techniques in our note-making project.
Debouncing ensures that a function is only called after a specified amount of time has passed since the last invocation. This is useful for scenarios like search input fields, where we want to wait for the user to finish typing before making an API call.
Code Snippet:
//Utility function to search within already entered values const searchTasks = (query) => { const result = fuse.search(query); const filteredTasks = result.map(result => result.item) updateDropdown(filteredTasks); }
Explanation:
const updateDropdown = (tasks) => { const dropdown = document.getElementById("dropdown"); dropdown.innerHTML = ""; if(tasks.length === 0) { dropdown.style.display = "none"; return; } tasks.forEach(task => { const listItem = document.createElement("li"); listItem.textContent = task; listItem.addEventListener("click", () => { document.getElementById("input").value = task; dropdown.style.display = "none"; }) dropdown.appendChild(listItem); }); dropdown.style.display = "block"; }
Explanation:
However, please note: Throttling is not the best fit for this scenario because it limits the frequency of function execution to a fixed interval, which might not provide the best user experience for real-time search suggestions. Users expect immediate feedback as they type, and throttling can introduce noticeable delays.
Throttling is more suitable for scenarios where you want to control the rate of event handling to avoid performance issues. Here are some examples:
By using throttling in these scenarios, you can improve performance and ensure a smoother user experience.
Find the Complete Code here
Happy Coding!
Please leave a feedback!
I hope you found this blog helpful! Your feedback is invaluable to me, so please leave your thoughts and suggestions in the comments below.
Feel free to connect with me on LinkedIn for more insights and updates. Let's stay connected and continue to learn and grow together!
The above is the detailed content of The art of Smooth UX : Debouncing and Throttling for a more performant UI. For more information, please follow other related articles on the PHP Chinese website!