Lesson 11-Frontend Algorithm Practice

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 call

Solution 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:

ScenarioUse
Search inputDebounce (wait for user to stop typing)
Scroll eventsThrottle (execute regularly during scroll)
Button click (prevent double-click)Throttle (allow at most once per second)
Window resizeDebounce (wait for resize to stop)
API rate limitingThrottle (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!

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here

Share your love