Real-World Coding Problems
Debounce and Throttle: The most-used algorithms in frontend
If there are two algorithms every frontend developer must understand, they’re debounce and throttle. These aren’t just “nice to know” — they’re essential for performance.
The problem: Events firing too frequently
// SCENARIO: Search input that fires on every keystroke
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (event) => {
searchAPI(event.target.value); // Fires on EVERY keystroke!
});
// Problem: If user types "javascript", you make 10 API calls!
Solution 1: Debounce
Debounce waits for the event to stop firing for a specified time before executing.
// NAIVE debounce implementation
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((event) => {
searchAPI(event.target.value);
}, 300)); // Wait 300ms after user stops typing
How debounce works (visual explanation):
Keystrokes: | j | a | v | a | (pause) | API call
Timeline: |---|---|---|---|---------|---------->
↑ ↑ ↑ ↑ ↑
timer timer timer timer Executes!
reset reset reset reset
sequenceDiagram
participant U as User
participant D as Debounce
participant A as API
U->>D: Keystroke 1
Note over D: Start timer (300ms)
U->>D: Keystroke 2
Note over D: Reset timer
U->>D: Keystroke 3
Note over D: Reset timer
Note over D: Timer expires (300ms of inactivity)
D->>A: Make API callSolution 2: Throttle
Throttle ensures the function executes at most once per specified time period.
// NAIVE throttle implementation
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Better implementation (executes at end of period too)
function throttleBetter(fn, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn.apply(this, args);
}
};
}
// BEST: Throttle with leading and trailing execution
function throttleBest(fn, limit) {
let waiting = false;
let lastCallTime = 0;
let timeoutId;
return function(...args) {
const now = Date.now();
if (!waiting) {
// Leading edge execution
fn.apply(this, args);
waiting = true;
lastCallTime = now;
timeoutId = setTimeout(() => {
waiting = false;
// Trailing edge execution
if (Date.now() - lastCallTime >= limit) {
fn.apply(this, args);
}
}, limit);
}
};
}
// Usage: Scroll handler
window.addEventListener('scroll', throttleBest(() => {
console.log('Scroll handler executed');
}, 100)); // At most once every 100ms
When to use which:
| Scenario | Use |
|---|---|
| Search input | Debounce (wait for user to stop typing) |
| Scroll events | Throttle (execute regularly during scroll) |
| Button click (prevent double-click) | Throttle (allow at most once per second) |
| Window resize | Debounce (wait for resize to stop) |
| API rate limiting | Throttle (ensure at most N calls per second) |
Real-world example: Autocomplete with debounce
class Autocomplete {
constructor(inputElement, options = {}) {
this.input = inputElement;
this.delay = options.delay || 300;
this.minLength = options.minLength || 2;
this.onResults = options.onResults || (() => {});
this.cache = new Map();
this.abortController = null;
this.setupListeners();
}
setupListeners() {
this.input.addEventListener('input', debounce((event) => {
this.handleInput(event.target.value);
}, this.delay));
}
async handleInput(query) {
if (query.length < this.minLength) {
this.onResults([]);
return;
}
// Check cache
if (this.cache.has(query)) {
this.onResults(this.cache.get(query));
return;
}
// Cancel previous request
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
try {
const results = await this.fetchResults(query, this.abortController.signal);
// Cache results
this.cache.set(query, results);
// Limit cache size
if (this.cache.size > 100) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.onResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
}
async fetchResults(query, signal) {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal
});
return response.json();
}
}
// Usage
const searchInput = document.getElementById('search');
new Autocomplete(searchInput, {
delay: 300,
minLength: 2,
onResults: (results) => {
// Update UI with results
displayResults(results);
}
});
Deep cloning algorithms
Deep cloning (creating a completely independent copy of an object) is a common need in frontend development.
The problem:
const original = { a: 1, b: { c: 2 } };
const shallow = { ...original };
shallow.b.c = 3;
console.log(original.b.c); // 3! (Shallow copy shares nested objects)
Solution 1: JSON methods (simple but limited)
function deepCloneJSON(obj) {
return JSON.parse(JSON.stringify(obj));
}
// Limitations:
// - Doesn't handle functions
// - Doesn't handle undefined
// - Doesn't handle Dates (becomes string)
// - Doesn't handle RegExp, Map, Set, etc.
// - Doesn't handle circular references (throws error)
Solution 2: Recursive clone (handles more types)
function deepClone(obj, visited = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle circular references
if (visited.has(obj)) {
return visited.get(obj);
}
// Handle Date
if (obj instanceof Date) {
return new Date(obj);
}
// Handle RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// Handle Array
if (Array.isArray(obj)) {
const clone = [];
visited.set(obj, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], visited);
}
return clone;
}
// Handle Map
if (obj instanceof Map) {
const clone = new Map();
visited.set(obj, clone);
for (const [key, value] of obj) {
clone.set(deepClone(key, visited), deepClone(value, visited));
}
return clone;
}
// Handle Set
if (obj instanceof Set) {
const clone = new Set();
visited.set(obj, clone);
for (const value of obj) {
clone.add(deepClone(value, visited));
}
return clone;
}
// Handle plain objects
const clone = Object.create(Object.getPrototypeOf(obj));
visited.set(obj, clone);
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], visited);
}
return clone;
}
Solution 3: Using the structured clone algorithm (modern browsers)
// Modern browsers support structuredClone
const cloned = structuredClone(original);
// Advantages:
// - Handles circular references
// - Handles Date, RegExp, Map, Set, etc.
// - Faster than custom implementation
// Limitations:
// - Doesn't handle functions
// - Doesn't handle DOM nodes
// - Not supported in older browsers (IE, older Safari)
Performance comparison:
function benchmarkCloneMethods() {
const obj = {
a: 1,
b: { c: 2, d: [1, 2, 3] },
e: new Date(),
f: new Map([['key', 'value']])
};
// Method 1: JSON
const start1 = performance.now();
for (let i = 0; i < 10000; i++) {
deepCloneJSON(obj);
}
const time1 = performance.now() - start1;
// Method 2: Recursive
const start2 = performance.now();
for (let i = 0; i < 10000; i++) {
deepClone(obj);
}
const time2 = performance.now() - start2;
// Method 3: structuredClone
const start3 = performance.now();
for (let i = 0; i < 10000; i++) {
structuredClone(obj);
}
const time3 = performance.now() - start3;
console.log(`JSON: ${time1.toFixed(2)}ms`);
console.log(`Recursive: ${time2.toFixed(2)}ms`);
console.log(`structuredClone: ${time3.toFixed(2)}ms`);
}
Flattening nested data structures
Frontend applications often receive deeply nested data from APIs that needs to be “flattened” for easier use.
The problem:
// API returns nested comments
const comments = [
{
id: 1,
text: 'Comment 1',
replies: [
{
id: 2,
text: 'Reply 1',
replies: [
{ id: 3, text: 'Reply to reply', replies: [] }
]
}
]
}
];
// You want a flat list: [comment1, reply1, replyToReply]
Solution: Recursive flatten
function flattenComments(comments) {
const result = [];
function flatten(items) {
for (const item of items) {
result.push(item);
if (item.replies && item.replies.length > 0) {
flatten(item.replies);
}
}
}
flatten(comments);
return result;
}
// USAGE: Maintain hierarchy level
function flattenWithLevel(comments, level = 0) {
const result = [];
function flatten(items, currentLevel) {
for (const item of items) {
result.push({ ...item, level: currentLevel });
if (item.replies && item.replies.length > 0) {
flatten(item.replies, currentLevel + 1);
}
}
}
flatten(comments, level);
return result;
}
Flattening arrays (different problem):
// Flatten array of arrays
const nested = [[1, 2], [3, 4], [5, 6]];
// Method 1: flat()
const flattened1 = nested.flat(); // [1, 2, 3, 4, 5, 6]
// Method 2: flatMap()
const flattened2 = nested.flatMap(x => x); // [1, 2, 3, 4, 5, 6]
// Method 3: Reduce
const flattened3 = nested.reduce((acc, val) => acc.concat(val), []);
// Deep flatten (arbitrarily nested)
const deeplyNested = [1, [2, [3, [4, [5]]]]];
// Method 1: flat(Infinity)
const deep1 = deeplyNested.flat(Infinity);
// Method 2: Recursive
function deepFlatten(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? deepFlatten(val) : val);
}, []);
}
// Method 3: Iterative with stack
function deepFlattenIterative(arr) {
const stack = [...arr];
const result = [];
while (stack.length > 0) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
result.push(next);
}
}
return result.reverse(); // Because we used stack (LIFO)
}
Tree traversal algorithms for component trees
React/Vue/Angular components form a tree. Understanding tree traversal is crucial for advanced frontend work.
The component tree:
<App>
<Header>
<Navigation />
</Header>
<Main>
<Sidebar />
<Content>
<Article />
<Comments />
</Content>
</Main>
<Footer />
</App>
Depth-First Search (DFS) – Pre-order
// Find component by type
function findComponent(root, type) {
if (!root) return null;
// Check current node
if (root.type === type) {
return root;
}
// Recursively search children
if (root.children) {
for (const child of root.children) {
const found = findComponent(child, type);
if (found) return found;
}
}
return null;
}
// USAGE in React (using React.Children)
function findChildByType(children, type) {
let found = null;
React.Children.forEach(children, (child) => {
if (child.type === type) {
found = child;
}
if (!found && child.props && child.props.children) {
found = findChildByType(child.props.children, type);
}
});
return found;
}
Breadth-First Search (BFS) – Level order
// BFS traversal of component tree
function traverseBFS(root) {
const queue = [root];
const result = [];
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
if (node.children) {
for (const child of node.children) {
queue.push(child);
}
}
}
return result;
}
Practical use case: Finding parent component
// Find parent of a given component
function findParent(root, target) {
if (!root || !root.children) return null;
for (const child of root.children) {
if (child === target) {
return root;
}
const found = findParent(child, target);
if (found) return found;
}
return null;
}
// In React DevTools, this is how "Find parent component" works!



