Learn how to optimize JavaScript performance using the Performance API, requestAnimationFrame, and techniques like debouncing and throttling for faster applications.
You’ve written code that works. Then you ship it, someone opens it on a mid-range phone, and it stutters. Closing that gap between “runs” and “feels fast” is what Day 11 is about.
Welcome back to our 30 Days of JavaScript series. Today is performance: how to measure it honestly, and a handful of techniques that buy real speed without a rewrite. None of it is exotic. It’s the everyday stuff that keeps an app responsive as the code and the data grow.
Table of Contents
- JavaScript Performance: Measuring Efficiency
- Animation Performance: Keeping in Step
- Debouncing and Throttling: Quality over Quantity
- Conclusion
JavaScript Performance: Measuring Efficiency
You can’t fix what you can’t measure, and guessing is how you burn an afternoon optimizing the wrong loop. So before you change anything, get a number. Is the code actually slow, or does it only feel slow? And where is the time going: a query, an algorithm, too many DOM writes? The Performance API is how you answer that instead of assuming.
Consider the following example:
/**
* Measures the time taken to execute a block of code.
*
* @returns {void}
*/
let startTime = performance.now();
// Execute some code here...
let endTime = performance.now();
console.log(`This code took ${endTime - startTime} milliseconds to execute.`);
The performance.now() function hands you a high-resolution timestamp in milliseconds, measured from when the page started loading. Bracket the code you suspect, subtract the two readings, and you have a real measurement instead of a hunch. It’s precise enough (well under a millisecond) to catch differences Date.now() would round away. Find the actual bottleneck first, then optimize that.
Animation Performance: Keeping in Step
Animation is where jank shows up first, because the eye is unforgiving. Land a frame late and people feel it even if they can’t name it. The requestAnimationFrame() method is the fix. Instead of guessing an interval with setTimeout(), you hand the browser a callback and it runs your function right before the next repaint, in step with the display’s refresh rate.
Here’s an example using requestAnimationFrame():
/**
* Animates an element by moving it across the screen using requestAnimationFrame.
*
* @param {DOMRect} elem - The DOM element to animate.
* @returns {void}
*/
let elem = document.getElementById('animate');
let startPos = 0;
let endPos = 300;
/**
* Updates the position of the element on each frame.
*
* @param {number} timestamp - The current time stamp provided by requestAnimationFrame.
*/
function step(timestamp) {
let progress = timestamp - startTime;
let currentPos = Math.min(startPos + progress / 10, endPos);
elem.style.left = currentPos + 'px';
if (currentPos < endPos) {
window.requestAnimationFrame(step);
}
}
let startTime = performance.now();
window.requestAnimationFrame(step);
Each frame, the browser calls step() just before it paints, so the motion lines up with the refresh cycle instead of fighting it. One caveat worth internalizing: drive the math with the timestamp the browser passes in (or performance.now()), the way the example does. Both are measured from the same time origin, so subtracting them is valid. Hard-code a fixed step per frame instead and the animation runs faster on a 120Hz screen than on a 60Hz one.
Debouncing and Throttling: Quality over Quantity
Some events fire far more often than you actually need. A resize handler can run dozens of times a second while someone drags a window corner. Debouncing and throttling both cut that down, and they solve slightly different problems.
Debouncing:
Debounce waits for the noise to stop. The function doesn’t run until a set amount of time has passed with no new calls. It’s a good fit for “when the user is finished”: done typing in a search field, done dragging the window to size.
/**
* Creates a debounced version of a function that delays its execution.
*
* @param {Function} func - The function to debounce.
* @param {number} wait - The amount of delay in milliseconds.
* @returns {Function} - The debounced function.
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
let context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
window.addEventListener('resize', debounce(() => {
console.log(window.innerWidth);
}, 250));
Here, the debounce() wrapper means the resize handler only fires 250 milliseconds after the last resize event. You react to the final size once, instead of to every size it passed through on the way.
Throttling:
Throttle is the opposite bargain. Rather than waiting for quiet, it lets the function run at most once per interval no matter how many events arrive. That’s what you want when you need steady updates during an ongoing action, like tracking scroll position.
/**
* Creates a throttled version of a function that limits its execution frequency.
*
* @param {Function} func - The function to throttle.
* @param {number} limit - The amount of time in milliseconds before the function can be called again.
* @returns {Function} - The throttled function.
*/
function throttle(func, limit) {
let inThrottle;
return function() {
let args = arguments;
let context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log(window.scrollY);
}, 250));
This throttle() wrapper runs the scroll handler at most once every 250 milliseconds: steady samples, far fewer calls. Quick rule of thumb: debounce when you only care about the last event, throttle when you want a regular sample of one that’s still happening.
Conclusion
That’s Day 11. Measure first with the Performance API so you’re fixing real bottlenecks, not the ones you imagined. Reach for requestAnimationFrame() whenever you’re animating. And keep debounce and throttle within reach for events that fire more often than you need them to.
None of this is a one-time cleanup. Code tends to get slower as it grows, and the fix is the same habit every time: measure, change one thing, measure again. Do that consistently and “fast” stops being luck and starts being something you can rely on.
What’s Next?
Day 12 is Working with JSON in JavaScript. You’ll parse it, reshape it, and move data in and out of APIs, which is the backbone of most data-driven apps. See you then.


