Skip to the code
cf.

Throttle

EasyTiming

Write throttle(fn, wait). It returns a function that runs fn at most once every wait ms. The first call runs fn right away, during the call, with that call's this and arguments.

Calls made less than wait ms after fn last ran are dropped: fn never runs for them, not even later. The first call made wait ms or more after fn last ran runs it again, right away.

const onScroll = throttle(() => console.log(window.scrollY), 100);
window.addEventListener("scroll", onScroll);
// logs right away, then at most once every 100 ms while you scroll
 
const log = throttle((n) => console.log(n), 100);
log(1); // at 0 ms: logs 1 right away
log(2); // at 50 ms: dropped, since fn ran 50 ms ago
log(3); // at 99 ms: dropped
log(4); // at 100 ms: logs 4
log(5); // at 150 ms: dropped
// 2, 3, and 5 are never logged, not even later

What the tests check

  1. runs fn right away on the first call
    useFakeTimers();
    const fn = spy();
    const throttled = throttle(fn, 100);
    throttled("a");
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith("a");
    
  2. drops the calls made during the wait
    const clock = useFakeTimers();
    const fn = spy();
    const throttled = throttle(fn, 100);
    throttled(1);
    clock.tick(50);
    throttled(2);
    clock.tick(49);
    throttled(3);
    clock.tick(1000);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenLastCalledWith(1);
    
  3. runs again once wait ms have passed
    const clock = useFakeTimers();
    const fn = spy();
    const throttled = throttle(fn, 100);
    throttled(1);
    clock.tick(100);
    throttled(2);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn).toHaveBeenLastCalledWith(2);
    
  4. measures the wait from the last run, not the last call
    const clock = useFakeTimers();
    const fn = spy();
    const throttled = throttle(fn, 100);
    throttled(1);
    clock.tick(60);
    throttled(2);
    clock.tick(40);
    throttled(3);
    expect(fn.calls.map((call) => call.args[0])).toEqual([1, 3]);
    
  5. runs at a steady pace during a stream of calls
    const clock = useFakeTimers();
    const start = clock.now();
    const runs = [];
    const throttled = throttle(() => runs.push(clock.now() - start), 100);
    for (let t = 0; t <= 300; t += 30) {
      throttled();
      clock.tick(30);
    }
    expect(runs).toEqual([0, 120, 240]);
    
  6. keeps the caller's this and arguments
    useFakeTimers();
    const fn = spy();
    const box = { onScroll: throttle(fn, 100) };
    box.onScroll(1, 2);
    expect(fn.calls[0].this).toBe(box);
    expect(fn).toHaveBeenLastCalledWith(1, 2);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).