JavaScript Performance Basics: Measure, Optimize, and Scale
Performance work is part engineering, part discipline. The best results come from measuring first, then optimizing the actual bottleneck. This guide focuses on practical JavaScript performance: algorithmic choices, memory behavior, caching, and safe micro-optimizations. Every section includes TypeScript examples and expected output.
Quick start
const items = Array.from({ length: 5 }, (_, i) => i + 1);
const sum = items.reduce((acc, v) => acc + v, 0);
console.log(sum);
Result:
15
Performance checklist
- Measure first: avoid guessing; confirm with timing.
- Pick the right data structure:
Mapvs object, arrays vs sets. - Reduce algorithmic complexity: O(n log n) can beat O(n^2) by orders of magnitude.
- Cache or memoize repeated work when inputs repeat.
- Avoid excessive allocations in hot loops.
- Batch DOM updates if you are in the browser.
Start with a performance baseline
Performance work should start with a baseline so you can prove improvements later. A baseline can be as simple as a timing measurement in a test script or a small benchmark in your dev environment. What matters most is that you compare the same code path before and after changes.
Measure with simple timers
In production, use real profilers, but local measurements still help validate assumptions. You can use performance.now() in browsers or Date.now() in any JS runtime.
function measure(label: string, fn: () => void): void {
const start = Date.now();
fn();
const end = Date.now();
console.log(`${label}: ${end - start}ms`);
}
measure("sum", () => {
let total = 0;
for (let i = 0; i < 1000; i += 1) total += i;
});
Result:
sum: 0ms
The exact number will vary, but the relative change is what matters.
Prefer the simplest algorithm that scales
Big O is not just theory. A slower algorithm can be fine for 100 items but catastrophic for 10,000. Start by estimating your data size and growth rate.
Choose the right algorithm
Small inputs hide slow algorithms. As data grows, complexity dominates everything else.
function hasDuplicateSlow(values: number[]): boolean {
for (let i = 0; i < values.length; i += 1) {
for (let j = i + 1; j < values.length; j += 1) {
if (values[i] === values[j]) return true;
}
}
return false;
}
function hasDuplicateFast(values: number[]): boolean {
const seen = new Set<number>();
for (const v of values) {
if (seen.has(v)) return true;
seen.add(v);
}
return false;
}
console.log(hasDuplicateFast([1, 2, 3, 2]));
Result:
true
The Set version is O(n) while the nested loop is O(n^2).
Data structures matter
Map has fast lookups and avoids prototype pitfalls. Use it for frequent inserts and reads.
const visits = new Map<string, number>();
visits.set("home", 1);
visits.set("home", (visits.get("home") ?? 0) + 1);
console.log(visits.get("home"));
Result:
2
Build strings efficiently
Repeated string concatenation in loops can create many intermediate strings. When you are assembling a known list of parts, Array.join is often cleaner and faster.
const parts = ["JavaScript", "performance", "matters"];
const sentence = parts.join(" ");
console.log(sentence);
Result:
JavaScript performance matters
Debounce expensive work
If an action fires frequently (like typing), debounce to reduce repeated work.
function debounce<TArgs extends unknown[]>(
fn: (...args: TArgs) => void,
delayMs: number
): (...args: TArgs) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return (...args: TArgs) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delayMs);
};
}
const save = debounce((value: string) => {
console.log(`saved: ${value}`);
}, 200);
save("draft-1");
Result:
saved: draft-1
Avoid extra allocations in hot loops
Creating new arrays or objects inside a tight loop can add pressure on the garbage collector.
function sumInline(values: number[]): number {
let total = 0;
for (let i = 0; i < values.length; i += 1) {
total += values[i];
}
return total;
}
console.log(sumInline([1, 2, 3, 4]));
Result:
10
Precompute repeated work
If the same calculation runs on every iteration, move it out of the loop.
const items = [1, 2, 3, 4, 5];
const factor = 10;
const scaled = items.map((n) => n * factor);
console.log(scaled);
Result:
[ 10, 20, 30, 40, 50 ]
Memoization for expensive work
Memoization trades memory for speed. Use it when the same inputs repeat.
function memoize<TArg, TResult>(fn: (arg: TArg) => TResult): (arg: TArg) => TResult {
const cache = new Map<TArg, TResult>();
return (arg: TArg) => {
if (cache.has(arg)) return cache.get(arg) as TResult;
const value = fn(arg);
cache.set(arg, value);
return value;
};
}
const slowSquare = (n: number) => n * n;
const fastSquare = memoize(slowSquare);
console.log(fastSquare(12));
Result:
144
Reduce work with early exits
Short-circuiting saves time and is easier to reason about.
function containsNegative(values: number[]): boolean {
for (const v of values) {
if (v < 0) return true;
}
return false;
}
console.log(containsNegative([1, 2, -1, 3]));
Result:
true
Use Sets for membership checks
Membership checks (includes) on arrays are O(n). For frequent lookups, a Set is typically faster.
const ids = new Set([10, 11, 12]);
console.log(ids.has(11));
Result:
true
Precompute lookup tables
If you repeatedly map codes to labels, a lookup table avoids branching logic.
const statusLabel: Record<number, string> = {
200: "OK",
404: "Not Found",
500: "Server Error",
};
console.log(statusLabel[404]);
Result:
Not Found
Prefer iterative solutions when recursion is deep
Recursive solutions can be elegant but may risk stack overflows for large inputs.
function factorialIterative(n: number): number {
let total = 1;
for (let i = 2; i <= n; i += 1) total *= i;
return total;
}
console.log(factorialIterative(6));
Result:
720
Batch work and avoid repeated parsing
If data is reused, parse once and reuse. This is especially relevant for JSON and CSV.
const jsonText = '{"items":[1,2,3,4]}';
const parsed = JSON.parse(jsonText) as { items: number[] };
const total = parsed.items.reduce((acc, v) => acc + v, 0);
console.log(total);
Result:
10
Browser note: avoid layout thrashing
If you are updating the DOM, batch reads and writes. Alternating read -> write -> read can force the browser to recalculate layout multiple times. Collect the measurements first, then apply updates in one pass. This is less about code syntax and more about how you structure the work.
Memory awareness
Performance is also about memory. Large arrays, caches, or retained objects can slow down GC. If you cache data, set limits or eviction strategies, and avoid retaining references to unused objects.
Common pitfalls
- Premature optimization: optimizing the wrong part yields no benefit.
- Micro-optimizing too early: readability often wins for non-hot code.
- Ignoring algorithmic complexity: O(n^2) becomes painful fast.
- Over-caching: memoization can blow up memory if inputs are unbounded.
- Benchmarking without control: run multiple times and warm up.
Best practices
- Profile before you optimize.
- Use the simplest solution that meets performance needs.
- Keep data structures consistent across the codebase.
- Prefer clarity until measurements prove otherwise.
- Document why a micro-optimization exists.
FAQ: JavaScript performance
How do I measure performance in JavaScript?
Use simple timers for quick checks, then rely on profilers for real analysis.
const start = Date.now();
for (let i = 0; i < 10000; i += 1) {}
const end = Date.now();
console.log(`${end - start}ms`);
Result:
0ms
What is the biggest performance win in JavaScript?
Usually an algorithmic change (like switching from O(n^2) to O(n log n)) beats any micro-optimization.
const fast = new Set([1, 2, 3, 4]);
console.log(fast.has(3));
Result:
true
Is memoization always good?
No. Memoization is great for repeated inputs but can consume a lot of memory for large input ranges.
const memo = new Map<number, number>();
memo.set(1, 1);
console.log(memo.size);
Result:
1
Summary
JavaScript performance is about measuring, choosing the right algorithms, and reducing unnecessary work. Start with profiling, then apply targeted changes that you can justify with data.