Ever wondered why your JavaScript app feels sluggish even though the code "works"? The truth is, many performance issues don't come from one big blunder they come from small mistakes that quietly pile up over time. A missed cleanup here, an extra re-render there, and suddenly your app that "works fine on my machine" is dropping frames on a mid-range phone.
In this article, we'll walk through 10 common JavaScript mistakes that can hurt your app's speed and, more importantly, how to fix them.
Why Performance Matters
- Better user experience - nobody enjoys a laggy interface
- Faster page loads - every second of delay increases bounce rate
- Improved Core Web Vitals - which directly affects how Google ranks you
- Lower CPU and memory usage - especially important on mobile and low-power devices
- Better SEO rankings - speed is a ranking signal
- Happier users and fewer support issues - performance bugs are UX bugs
Let's get into it.
Mistake #1: Manipulating the DOM Too Frequently
Why it's slow
The DOM is expensive. Every time you touch it even to read a property like offsetHeight the browser may need to recalculate layout (a "reflow") and repaint the screen. Do this inside a loop, and you're forcing the browser to redo this work dozens or hundreds of times in a single frame.
❌ Common mistake
const list = document.getElementById("list");
for (let i = 0; i < items.length; i++) {
const li = document.createElement("li");
li.textContent = items[i];
list.appendChild(li); // triggers a reflow on every iteration
}
✅ Better approach
Batch your changes using a DocumentFragment, so the browser only has to reflow once:
const list = document.getElementById("list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i++) {
const li = document.createElement("li");
li.textContent = items[i];
fragment.appendChild(li);
}
list.appendChild(fragment); // single reflow
Key takeaway
Batch DOM writes, avoid reading layout properties inside loops, and think in terms of "one big update" instead of many small ones.
Mistake #2: Blocking the Main Thread with Heavy Computation
Why it's slow
JavaScript in the browser runs on a single main thread the same thread responsible for rendering, handling clicks, and scrolling. If you run a long, synchronous computation, everything else freezes until it finishes.
❌ Common mistake
function processLargeDataset(data) {
let result = [];
for (let i = 0; i < data.length; i++) {
result.push(expensiveTransform(data[i])); // blocks the UI
}
return result;
}
✅ Use Web Workers or break tasks into chunks
Offload heavy work to a Web Worker so it runs off the main thread:
// worker.js
self.onmessage = (e) => {
const result = e.data.map(expensiveTransform);
self.postMessage(result);
};
// main.js
const worker = new Worker("worker.js");
worker.postMessage(largeDataset);
worker.onmessage = (e) => renderResults(e.data);
If a worker isn't practical, break the work into chunks using requestIdleCallback or setTimeout so the browser can breathe between chunks.
Key takeaway
Never let a single synchronous task hog the main thread. Offload or chunk it.
Mistake #3: Making Too Many API Requests
Why it's slow
Every network request has overhead DNS lookup, TLS handshake, server processing, and payload transfer. Firing off dozens of redundant requests (especially from re-renders or user input) adds up fast and can overwhelm both the client and the server.
❌ Common mistake
searchInput.addEventListener("input", (e) => {
fetch(`/api/search?q=${e.target.value}`); // fires on every keystroke
});
✅ Debounce, throttle, cache, or batch requests
let timeoutId;
searchInput.addEventListener("input", (e) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fetch(`/api/search?q=${e.target.value}`);
}, 300); // waits until typing pauses
});
Consider caching responses (with something like stale-while-revalidate) and batching multiple small requests into one, if your backend supports it.
Key takeaway
Treat every network call as expensive. Debounce user-triggered requests, cache what you can, and batch where possible.
Mistake #4: Ignoring Debounce and Throttle for User Events
Some browser events fire far more often than your UI actually needs to respond to them:
- Search inputs
- Scroll events
- Window resize
- Mouse move
Why it's slow
A scroll or resize listener can fire dozens of times per second. If your handler does expensive work like recalculating layout or updating state you're doing that work far more often than necessary.
❌ Common mistake
window.addEventListener("resize", () => {
recalculateLayout(); // runs constantly while resizing
});
✅ Better approach
Throttle the handler so it only runs at a controlled interval:
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
window.addEventListener("resize", throttle(recalculateLayout, 200));
Use debounce when you only care about the final event (like "user stopped typing"), and throttle when you want steady, periodic updates (like "user is scrolling").
Key takeaway
Match your event-handling strategy to the event: debounce for "settled" states, throttle for "ongoing" states.
Mistake #5: Creating Memory Leaks
Common culprits include:
- Forgotten event listeners
- Timers that are never cleared
- Detached DOM elements still referenced in JS
- Closures holding unnecessary references
Why it's slow
Memory leaks don't crash your app immediately they degrade it over time. As memory usage climbs, garbage collection runs more often and takes longer, causing stutters and eventual crashes, especially in long-running single-page apps.
❌ Common mistake
function setup() {
const bigData = new Array(1000000).fill("leak");
window.addEventListener("resize", () => {
console.log(bigData.length); // closure keeps bigData alive forever
});
}
✅ Better approach
Clean up listeners and timers when they're no longer needed:
function setup() {
const controller = new AbortController();
window.addEventListener("resize", handleResize, {
signal: controller.signal,
});
// later, when the component/page unmounts:
controller.abort(); // removes the listener automatically
}
In frameworks, always clean up inside the appropriate lifecycle hook (e.g., a useEffect cleanup function in React).
Key takeaway
Every listener, timer, or subscription you add should have a corresponding removal. If you're not sure it's cleaned up, assume it's leaking.
Mistake #6: Inefficient Loops and Array Operations
Watch out for:
- Nested loops (O(n²) when O(n) would do)
- Unnecessary
map()calls that aren't used - Multiple
filter()calls that could be combined - Expensive operations repeated inside loops
❌ Common mistake
const activeUserNames = users
.filter((u) => u.active)
.filter((u) => !u.deleted)
.map((u) => u.name); // three full passes over the array
✅ Optimized approach
const activeUserNames = [];
for (const u of users) {
if (u.active && !u.deleted) {
activeUserNames.push(u.name); // one pass
}
}
For lookups inside loops, prefer a Map or Set over Array.includes(), which is O(n) per check:
const idSet = new Set(validIds);
const filtered = items.filter((item) => idSet.has(item.id)); // O(1) lookups
❌ Common mistake
import HeavyChart from "./HeavyChart";
import AdminPanel from "./AdminPanel";
import ExportTools from "./ExportTools";
// all loaded immediately, even if the user never visits these views
✅ Better approach
const HeavyChart = React.lazy(() => import("./HeavyChart"));
const AdminPanel = React.lazy(() => import("./AdminPanel"));
// only fetched when the route/component is actually needed
Pair this with loading="lazy" on images and modern formats (WebP/AVIF) with proper srcset sizing.
Key takeaway
Load what's needed now, and defer the rest. Your initial bundle should be as small as the first screen requires.
Mistake #8: Re-rendering More Than Necessary
This applies across:
- React
- Vue
- Angular
- Vanilla JavaScript
Why it's slow
Frameworks make UI updates easy, but that ease can hide the cost: every unnecessary re-render means recalculating a virtual DOM tree (or worse, touching the real DOM) for something that didn't actually need to change.
Ways to fix it:
-
Memoization -
React.memo,useMemo, anduseCallbackprevent components and values from being recreated unnecessarily. - Component optimization - keep components focused, and lift state only as high as it needs to be.
- Avoid unnecessary DOM updates - in vanilla JS, diff values before writing to the DOM instead of writing unconditionally.
// Vanilla JS: only update the DOM if the value actually changed
function updateCounter(newValue) {
if (counterEl.textContent !== String(newValue)) {
counterEl.textContent = newValue;
}
}
Key takeaway
More renders isn't more responsivenes it's wasted work. Render only when something the user can see has actually changed.
Mistake #9: Not Profiling Before Optimizing
Tools worth knowing:
- Chrome DevTools Performance Panel - precords exactly where time is spent during a session
- Lighthouse - audits performance, accessibility, and best practices
- Memory Tab - takes heap snapshots to spot leaks
- Performance Monitor - watches CPU, memory, and DOM node count in real time
Why it matters
It's tempting to guess at what's slow and "optimize" it but guessing often means spending hours micro-optimizing code that was never the bottleneck in the first place. Profiling tells you exactly where the time and memory are going, so your effort lands where it actually matters.
A simple rule: measure first, optimize second, measure again to confirm the fix worked.
Key takeaway
Don't optimize what you haven't measured. Let the profiler point you to the real bottleneck.
Mistake #10: Trusting AI-Generated Code Without Performance Review
Modern AI tools can generate working code in seconds - but "working" and "efficient" aren't the same thing. AI-generated code can quietly introduce:
- Extra loops that repeat work already done elsewhere
- Redundant calculations recomputed on every render or call
- Duplicate API calls triggered by copy-pasted logic
- Unnecessary state updates that trigger extra re-renders
Why it matters
AI models optimize for producing correct-looking code quickly, not necessarily the most performant version of that code. If you paste AI output straight into production without reading it, you may be shipping subtle performance regressions you'd never write yourself and that are easy to miss in code review because the code "looks fine."
Always read AI-generated code with the same performance lens you'd apply to a human PR: Is this loop necessary? Is this value recalculated when it could be cached? Is this the same API call I'm already making somewhere else?
Key takeaway
AI-generated code is a first draft, not a finished product. Review it for performance the same way you'd review any other code before it ships.
🚀 Bonus Tips for Faster JavaScript Apps
- Minify JavaScript
- Use tree shaking to eliminate dead code
- Apply code splitting aggressively
- Use
requestAnimationFrame()for animations instead ofsetTimeout - Optimize images (compression, modern formats, correct sizing)
- Cache API responses where appropriate
- Use a CDN for static assets
- Avoid unnecessary dependencies every package adds parse and execution cost
Performance Checklist ✅
- [ ] Minimize DOM manipulation
- [ ] Debounce expensive events
- [ ] Remove unused event listeners
- [ ] Cache API requests
- [ ] Lazy-load resources
- [ ] Avoid blocking the main thread
- [ ] Optimize loops
- [ ] Measure before optimizing
- [ ] Profile memory usage
- [ ] Review AI-generated code before shipping
Final Thoughts
Performance isn't about writing clever code it's about avoiding common mistakes that slowly degrade the user experience. Small improvements add up, and consistently applying good practices can make your applications feel noticeably faster and more responsive.
Continue Reading
If you found this article helpful, you might also enjoy this:
What do you think?
- Which JavaScript performance mistake have you encountered most often?
- What's your favorite performance optimization technique?
- Have you discovered a tip that significantly improved your app's speed?
Drop your thoughts in the comments below 👇
Top comments (0)