Day 27: Performance & Memory
Day 27: Performance & Memory
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand how JavaScript manages memory (stack vs heap, garbage collection)
- Identify and fix common memory leaks in web applications
- Optimize DOM manipulations to minimize reflows and repaints
- Use performance measurement tools (
performance.now(),console.time, Chrome DevTools) - Apply debouncing and throttling to limit expensive operations
- Implement code splitting and lazy loading for faster initial page loads
- Use Web Workers to offload heavy computations from the main thread
- Write memory-efficient code by choosing appropriate data structures
2. Theory
Memory Management in JavaScript
JavaScript uses automatic memory management via garbage collection. The two main memory areas are:
| Area | Stores | Speed | Size |
|---|---|---|---|
| Stack | Primitive values, function call frames, variable references | Fast | Small (limited) |
| Heap | Objects, arrays, functions, closures | Slower | Large (limited by system) |
How Garbage Collection Works
The most common algorithm is mark-and-sweep:
- Mark — Starting from "roots" (global object, current function scope, DOM elements), the GC marks all reachable objects.
- Sweep — All unmarked objects are considered unreachable and their memory is freed.
// Objects are kept alive as long as they are reachable
let user = { name: "Alice" }; // Reachable via global 'user'
user = null; // Now unreachable → eligible for GC
Reference Counting (Simplified)
let a = { name: "A" };
let b = { name: "B" };
a.ref = b; // b has 2 references (b, a.ref)
b.ref = a; // a has 2 references (a, b.ref)
a = null; // a has 1 reference (b.ref)
b = null; // b has 1 reference (a.ref — but a is null!)
// Circular reference! Mark-and-sweep handles this; reference counting alone would not.
Common Memory Leaks
1. Accidental Global Variables
function leak() {
leaked = "I'm global!"; // No 'let' or 'const' — becomes global
}
leak();
console.log(window.leaked); // "I'm global!" — never GC'd
Fix: Always use "use strict" or declare variables with let/const.
2. Forgotten Timers and Intervals
function startTimer() {
setInterval(() => {
// Do something with DOM elements
const data = fetchExpensiveData();
document.querySelector("#output").textContent = data;
}, 1000);
}
// Even after removing the #output element, the interval keeps running
// It holds a reference to the old callback and any captured variables
Fix: Always clear timers when they are no longer needed:
const timer = setInterval(fn, 1000);
// Later:
clearInterval(timer);
3. Detached DOM Nodes
const parent = document.getElementById("parent");
const child = document.getElementById("child");
parent.removeChild(child); // Removed from DOM
// But if JavaScript still holds a reference to 'child', memory is not freed
// child variable still references the detached element
child = null; // Now eligible for GC
4. Closures Holding Large Data
function createLeakyClosure() {
const largeData = new Array(1000000).fill("data");
return function() {
console.log(largeData.length);
};
}
const leaky = createLeakyClosure();
// largeData is kept alive as long as leaky exists
Fix: Only capture what you need, or nullify large data when done.
5. Event Listeners Not Removed
function addHandler() {
const button = document.getElementById("btn");
button.addEventListener("click", onClick);
// If button is removed from DOM without removing listener, both stay in memory
}
Fix: Use removeEventListener when elements are removed.
Performance Optimization Techniques
Debouncing and Throttling
| Technique | Description | Use Case |
|---|---|---|
| Debounce | Delays execution until after a pause in calls | Search input, window resize |
| Throttle | Limits execution to at most once per interval | Scroll events, mousemove |
// Debounce (from Day 12)
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle (from Day 12)
function throttle(fn, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
DOM Reflows and Repaints
| Operation | What Happens | Cost |
|---|---|---|
| Reflow | Browser recalculates layout positions/sizes | Expensive |
| Repaint | Browser redraws pixels (no layout change) | Cheaper |
Causes of reflow:
- Adding/removing DOM elements
- Changing element dimensions (width, height, margin, padding)
- Changing font, text content
- Resizing the window
- Reading layout properties (offsetTop, offsetHeight, etc.) — forces a reflow if there are pending changes
Best Practices to Minimize Reflows:
// BAD: Causes 3 separate reflows
element.style.width = "100px";
element.style.height = "100px";
element.style.margin = "10px";
// GOOD: Batch style changes — 1 reflow
element.style.cssText = "width: 100px; height: 100px; margin: 10px;";
// BEST: Use CSS classes — 1 reflow
element.classList.add("box");
// BAD: Reading layout after DOM mutation (forces reflow)
element.style.width = "200px";
const width = element.offsetWidth; // Forces immediate reflow!
// GOOD: Read before mutation, then mutate
const oldWidth = element.offsetWidth;
element.style.width = "200px";
// Use document fragments for batch inserts
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment); // Single reflow instead of 1000
Measuring Performance
// console.time / console.timeEnd
console.time("array operation");
const arr = new Array(1000000).fill(0).map((_, i) => i * 2);
console.timeEnd("array operation");
// performance.now() — high-resolution timestamp
const start = performance.now();
heavyComputation();
const end = performance.now();
console.log(`Took ${end - start}ms`);
// performance.mark() and performance.measure()
performance.mark("start");
heavyComputation();
performance.mark("end");
performance.measure("computation", "start", "end");
const entries = performance.getEntriesByName("computation");
console.log(`Took ${entries[0].duration}ms`);
Web Workers
Web Workers allow you to run JavaScript in a separate thread, keeping the UI responsive during heavy computations.
// ---- worker.js ----
self.addEventListener("message", (event) => {
const { data } = event;
// Perform heavy computation
const result = data.numbers.map(n => {
// Simulate heavy work
let sum = 0;
for (let i = 0; i < n * 10000; i++) sum += Math.sqrt(i);
return sum;
});
self.postMessage({ result });
});
// ---- main.js ----
const worker = new Worker("worker.js");
worker.addEventListener("message", (event) => {
console.log("Result from worker:", event.data.result);
worker.terminate(); // Clean up
});
worker.postMessage({ numbers: [1000, 2000, 3000] });
Limitations:
- No access to DOM,
window, ordocument - Communication is via messages (copy, not shared memory)
- Workers have their own global scope (
self) - Creating workers has overhead — only use for significant computations
3. Code Examples
Example 1: Detecting and Fixing Memory Leaks
// ---- Memory Leak Examples ----
// Leak 1: Accidental global
function createLeak1() {
accidentalGlobal = "I leak!"; // No declaration keyword
}
createLeak1();
console.log(window.accidentalGlobal); // Still exists
// Fix: "use strict" or let/const
// Leak 2: Forgotten interval
function createLeak2() {
const intervalId = setInterval(() => {
console.log("Still running...");
}, 1000);
// Never cleared!
return intervalId; // Return it so caller can clear
}
// Fix: Always clear when done
const id = createLeak2();
// Later:
// clearInterval(id);
// Leak 3: Detached DOM nodes
function createLeak3() {
const div = document.createElement("div");
div.id = "leaky";
document.body.appendChild(div);
const removed = document.body.removeChild(div);
// 'removed' still holds a reference — not GC'd
// Fix: set to null when done
// removed = null;
}
// Leak 4: Closure capturing large data
function createLeak4() {
const heavyData = new Array(100000).fill("data");
return {
getDataLength: () => heavyData.length,
clearData: () => { heavyData.length = 0; } // Allow GC
};
}
const obj = createLeak4();
console.log(obj.getDataLength()); // 100000
// When done:
obj.clearData(); // Now heavyData can be GC'd
// ---- Memory Leak Detector ----
function findDetachedNodes() {
const allNodes = document.querySelectorAll("*");
const detached = [];
allNodes.forEach(node => {
if (!document.contains(node) && node.parentNode === null) {
detached.push(node);
}
});
return detached;
}
// Use Chrome DevTools Memory tab for detailed analysis
// 1. Take a heap snapshot
// 2. Perform actions
// 3. Take another snapshot
// 4. Compare to find retained objects
Example 2: Performance Measurement
// ---- Measuring Performance ----
function heavyComputation(size) {
let result = 0;
for (let i = 0; i < size; i++) {
result += Math.sqrt(i * Math.sin(i));
}
return result;
}
// Method 1: console.time
console.time("heavy computation");
heavyComputation(10000000);
console.timeEnd("heavy computation");
// Method 2: performance.now()
const start = performance.now();
heavyComputation(10000000);
const end = performance.now();
console.log(`Heavy computation took ${(end - start).toFixed(2)}ms`);
// Method 3: performance.mark and measure (Chrome DevTools)
function measurePerformance() {
performance.mark("start-task");
const result = heavyComputation(5000000);
performance.mark("end-task");
performance.measure("task", "start-task", "end-task");
const entries = performance.getEntriesByName("task");
console.log(`Measured task: ${entries[0].duration.toFixed(2)}ms`);
// Clean up marks
performance.clearMarks();
performance.clearMeasures();
return result;
}
measurePerformance();
// ---- Comparing Approaches ----
console.log("\n--- Comparing Array Methods ---\n");
const testArray = Array.from({ length: 100000 }, (_, i) => i);
// For loop vs forEach vs reduce for summation
function testForLoop(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
function testForOf(arr) {
let sum = 0;
for (const val of arr) {
sum += val;
}
return sum;
}
function testReduce(arr) {
return arr.reduce((sum, val) => sum + val, 0);
}
function testForEach(arr) {
let sum = 0;
arr.forEach(val => { sum += val; });
return sum;
}
// Warm up
[testForLoop, testForOf, testReduce, testForEach].forEach(fn => fn(testArray.slice(0, 1000)));
// Time each
const tests = [
{ name: "for loop", fn: testForLoop },
{ name: "for...of", fn: testForOf },
{ name: "reduce", fn: testReduce },
{ name: "forEach", fn: testForEach },
];
tests.forEach(({ name, fn }) => {
const start = performance.now();
for (let i = 0; i < 10; i++) fn(testArray);
const end = performance.now();
console.log(`${name}: ${((end - start) / 10).toFixed(4)}ms (avg)`);
});
Example 3: DOM Optimization
// ---- DOM Performance Optimization ----
// BAD: Multiple individual DOM manipulations
function createListBad(items) {
const list = document.getElementById("list");
list.innerHTML = ""; // Clears — causes reflow
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item;
list.appendChild(li); // Each append causes reflow
});
}
// GOOD: Using DocumentFragment
function createListGood(items) {
const list = document.getElementById("list");
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item;
fragment.appendChild(li);
});
list.innerHTML = ""; // Could also use list.replaceChildren()
list.appendChild(fragment); // Single reflow
}
// BEST: Using innerHTML with string building (for simple cases)
function createListBest(items) {
const list = document.getElementById("list");
list.innerHTML = items.map(item => `<li>${item}</li>`).join("");
}
// ---- Avoiding Layout Thrashing ----
function layoutThrashing() {
const boxes = document.querySelectorAll(".box");
// BAD: Read/write interleaved (forces reflow on every iteration)
boxes.forEach(box => {
const width = box.offsetWidth; // Read (forces reflow)
box.style.width = (width + 10) + "px"; // Write
});
// GOOD: Batch reads, then batch writes
const widths = [];
boxes.forEach(box => {
widths.push(box.offsetWidth); // Read all
});
boxes.forEach((box, i) => {
box.style.width = (widths[i] + 10) + "px"; // Write all
});
}
// ---- requestAnimationFrame for Visual Updates ----
function smoothUpdate(element, targetPosition) {
let current = 0;
const step = 5;
function animate() {
current += step;
if (current >= targetPosition) {
element.style.transform = `translateX(${targetPosition}px)`;
return;
}
element.style.transform = `translateX(${current}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
Example 4: Web Workers
// ---- worker.js (separate file) ----
self.addEventListener("message", (event) => {
const { type, data } = event.data;
if (type === "compute") {
const result = performHeavyComputation(data);
self.postMessage({ type: "result", data: result });
} else if (type === "processArray") {
const result = data.map(n => n * n);
self.postMessage({ type: "processed", data: result });
}
});
function performHeavyComputation(iterations) {
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i);
}
return result;
}
// ---- main.js ----
function createWorker() {
const worker = new Worker("worker.js");
worker.addEventListener("message", (event) => {
const { type, data } = event.data;
console.log(`Worker result (${type}):`, data);
});
worker.addEventListener("error", (error) => {
console.error("Worker error:", error.message);
});
return worker;
}
// Usage
const worker = createWorker();
worker.postMessage({ type: "compute", data: 10000000 }); // Heavy work offloaded
console.log("Main thread is still responsive!");
// Terminate when done
// worker.terminate();
// ---- Pool of Workers ----
class WorkerPool {
constructor(workerScript, size = 4) {
this.workers = [];
this.idle = [];
this.queue = [];
this.results = new Map();
for (let i = 0; i < size; i++) {
const worker = new Worker(workerScript);
worker.id = i;
worker.busy = false;
worker.addEventListener("message", (event) => {
worker.busy = false;
this.idle.push(worker);
this.processNext();
const resolve = this.results.get(event.data.id);
if (resolve) {
resolve(event.data);
this.results.delete(event.data.id);
}
});
this.workers.push(worker);
this.idle.push(worker);
}
}
exec(data) {
return new Promise((resolve) => {
const id = Date.now() + Math.random();
this.results.set(id, resolve);
this.queue.push({ id, data });
this.processNext();
});
}
processNext() {
if (this.queue.length === 0 || this.idle.length === 0) return;
const task = this.queue.shift();
const worker = this.idle.pop();
worker.busy = true;
worker.postMessage(task);
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Usage
// const pool = new WorkerPool("worker.js", 4);
// const result = await pool.exec({ type: "compute", data: 10000000 });
Example 5: Performance Profiling with Chrome DevTools
// ---- Code to Profile in Chrome DevTools ----
// 1. Open Chrome DevTools (F12)
// 2. Go to Performance tab
// 3. Click "Record" button
// 4. Run the code below
// 5. Stop recording
// 6. Analyze the flame chart, look for long tasks, forced reflows, etc.
function simulateApp() {
console.log("=== Performance Profiling Demo ===\n");
// Create a large dataset
const data = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
age: Math.floor(Math.random() * 50) + 18
}));
console.time("process data");
// Expensive transformation
const processed = data
.filter(user => user.age >= 21)
.map(user => ({
...user,
name: user.name.toUpperCase(),
isAdult: user.age >= 21
}))
.sort((a, b) => a.age - b.age);
console.timeEnd("process data");
// Simulate DOM operations
const container = document.createElement("div");
container.id = "profile-container";
document.body.appendChild(container);
console.time("render DOM");
// BAD: Individual appends (causes reflows)
// processed.slice(0, 500).forEach(user => {
// const div = document.createElement("div");
// div.textContent = `${user.name} (${user.age})`;
// container.appendChild(div);
// });
// GOOD: Use fragment
const fragment = document.createDocumentFragment();
processed.slice(0, 500).forEach(user => {
const div = document.createElement("div");
div.textContent = `${user.name} (${user.age})`;
fragment.appendChild(div);
});
container.appendChild(fragment);
console.timeEnd("render DOM");
// Simulate layout thrashing
console.time("layout thrashing");
const items = container.children;
for (let i = 0; i < items.length; i++) {
// Reading offsetHeight forces reflow if there are pending writes
const height = items[i].offsetHeight;
// But we're not even using the value — wasted reflow!
}
console.timeEnd("layout thrashing");
console.log("Profile complete. Check Performance tab.");
}
// Run in browser console with DevTools Performance tab open
// simulateApp();
4. Exercises
Beginner
- Use
console.time()andconsole.timeEnd()to measure how long it takes to sum numbers from 1 to 1,000,000 with aforloop. - Create a memory leak by creating a
setIntervalthat never gets cleared. Identify it in Chrome DevTools Memory tab. - Use
performance.now()to measure the execution time of a function that creates an array of 1,000,000 zeros.
Intermediate
- Write a function
batchedDOMUpdate(elements, container)that adds an array of elements to the DOM using a single DocumentFragment instead of individual appends. Compare performance withconsole.time. - Create a debounced search function that only sends a fetch request after the user has stopped typing for 300ms.
- Use Chrome DevTools Performance tab to record and analyze a page with layout thrashing. Fix the issue and compare before/after.
Advanced
Build a memory leak detector utility that:
- Wraps
setInterval,addEventListener, and DOM creation - Tracks all active references
- Provides a
getLeaks()method that returns potential leaks - Provides a
cleanup()method that clears timers and removes listeners
- Wraps
Implement a Web Worker-based image processor that:
- Takes an array of image URLs
- Downloads each image in the main thread (or uses
fetch) - Sends pixel data to Workers for processing (e.g., grayscale, blur)
- Returns the processed data to the main thread for display
5. Mini Project: Performance Dashboard
Build a dashboard that monitors and reports performance metrics for a simulated application.
// === Performance Dashboard ===
// ---- Performance Monitor ----
class PerformanceMonitor {
constructor() {
this.metrics = {
fps: [],
memory: [],
timing: {}
};
this.frameCount = 0;
this.lastFrameTime = performance.now();
this.isRunning = false;
}
start() {
this.isRunning = true;
this.metrics.fps = [];
this.metrics.memory = [];
this.lastFrameTime = performance.now();
this.frameCount = 0;
this._tick();
this._sampleMemory();
}
stop() {
this.isRunning = false;
}
_tick() {
if (!this.isRunning) return;
this.frameCount++;
const now = performance.now();
const delta = now - this.lastFrameTime;
if (delta >= 1000) {
const fps = Math.round((this.frameCount * 1000) / delta);
this.metrics.fps.push(fps);
this.lastFrameTime = now;
this.frameCount = 0;
}
requestAnimationFrame(() => this._tick());
}
_sampleMemory() {
if (!this.isRunning) return;
if (performance.memory) {
this.metrics.memory.push({
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
timestamp: Date.now()
});
}
setTimeout(() => this._sampleMemory(), 2000);
}
measure(name, fn) {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
this.metrics.timing[name] = duration;
return result;
}
async measureAsync(name, fn) {
const start = performance.now();
const result = await fn();
const duration = performance.now() - start;
this.metrics.timing[name] = duration;
return result;
}
getReport() {
const avgFps = this.metrics.fps.length > 0
? Math.round(this.metrics.fps.reduce((a, b) => a + b, 0) / this.metrics.fps.length)
: 0;
const avgMemory = this.metrics.memory.length > 0
? Math.round(
this.metrics.memory.reduce((sum, m) => sum + m.usedJSHeapSize, 0) /
this.metrics.memory.length /
(1024 * 1024)
)
: 0;
return {
fps: {
current: this.metrics.fps[this.metrics.fps.length - 1] || 0,
average: avgFps,
samples: this.metrics.fps.length,
min: Math.min(...this.metrics.fps) || 0,
max: Math.max(...this.metrics.fps) || 0
},
memory: {
averageMB: avgMemory,
samples: this.metrics.memory.length,
latestMB: this.metrics.memory.length > 0
? Math.round(this.metrics.memory[this.metrics.memory.length - 1].usedJSHeapSize / (1024 * 1024))
: 0
},
timing: this.metrics.timing,
recommendations: this._generateRecommendations()
};
}
_generateRecommendations() {
const recs = [];
if (this.metrics.fps.length > 0) {
const avg = this.metrics.fps.reduce((a, b) => a + b, 0) / this.metrics.fps.length;
if (avg < 30) recs.push("⚠️ Low FPS detected. Consider reducing DOM complexity or using Web Workers.");
if (avg < 60) recs.push("💡 FPS below 60. Consider optimizing animations with requestAnimationFrame.");
}
Object.entries(this.metrics.timing).forEach(([name, duration]) => {
if (duration > 100) recs.push(`⏱️ "${name}" took ${duration.toFixed(0)}ms. Consider chunking or Web Workers.`);
});
if (recs.length === 0) recs.push("✅ No performance issues detected.");
return recs;
}
}
// ---- Simulated Application ----
class SimulatedApp {
constructor() {
this.monitor = new PerformanceMonitor();
this.data = [];
}
start() {
this.monitor.start();
console.log("Performance monitoring started. Running simulated workload...");
this._simulateWorkload();
}
stop() {
this.monitor.stop();
console.log("Monitoring stopped.");
console.log("Performance Report:", this.monitor.getReport());
}
async _simulateWorkload() {
// Simulate periodic heavy operations
for (let i = 0; i < 5; i++) {
await this._doHeavyWork();
await this._simulateDOMUpdates();
await new Promise(resolve => setTimeout(resolve, 500));
}
this.stop();
}
async _doHeavyWork() {
this.monitor.measure("heavy computation", () => {
let sum = 0;
for (let i = 0; i < 5000000; i++) {
sum += Math.sin(i) * Math.cos(i);
}
return sum;
});
}
async _simulateDOMUpdates() {
this.monitor.measure("DOM update", () => {
const container = document.createElement("div");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
div.style.padding = "2px";
fragment.appendChild(div);
}
container.appendChild(fragment);
document.body.appendChild(container);
// Read layout (bad practice)
const heights = [];
for (let i = 0; i < container.children.length; i++) {
heights.push(container.children[i].offsetHeight);
}
// Clean up
document.body.removeChild(container);
return heights.length;
});
}
}
// ---- Run the Dashboard ----
console.log("=== Performance Dashboard ===\n");
const app = new SimulatedApp();
app.start(); // Will auto-stop after all simulations
// You can also get a report manually:
// setTimeout(() => {
// const report = app.monitor.getReport();
// console.log("Intermediate report:", report);
// }, 3000);
Try extending it: Add a visual dashboard (HTML/CSS) that renders FPS chart, memory usage graph, and timing breakdown. Implement warning thresholds and real-time alerts.
6. Common Mistakes
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
| Creating objects inside hot loops | Generates garbage, triggering GC more often | Create objects outside loops or reuse them |
| Reading layout properties after DOM mutations | Forces synchronous reflow, hurting performance | Batch reads and writes separately |
Using innerHTML for complex updates | Parses HTML, destroys and recreates all child nodes | Use createElement and fragments for complex updates |
| Forgetting to clean up timers and listeners | Causes memory leaks and zombie callbacks | Always pair setInterval with clearInterval, addEventListener with removeEventListener |
| Using too many event listeners | Each listener uses memory; hundreds slow down interaction | Use event delegation for similar elements |
| Storing large data in closures unnecessarily | Prevents GC from collecting that data | Nullify references when no longer needed or restructure code |
| Not using Web Workers for heavy computation | Blocks the main thread, freezing the UI | Offload CPU-intensive work to Web Workers |
| Over-optimizing prematurely | Wastes development time on code that doesn't need optimization | Profile first, then optimize the bottlenecks |
7. Best Practices
- Profile before optimizing — use Chrome DevTools Performance/Memory tabs to find real bottlenecks.
- Avoid blocking the main thread — yield with
setTimeout(0)or use Web Workers for heavy tasks. - Minimize DOM access — cache DOM references, batch reads and writes, use DocumentFragment.
- Use
requestAnimationFramefor visual updates — it synchronizes with the browser's paint cycle. - Debounce or throttle expensive event handlers (scroll, resize, input).
- Clean up after yourself — clear timers, remove event listeners, nullify large references.
- Choose appropriate data structures — Maps for frequent key lookups, Sets for unique values, typed arrays for numeric data.
- Use
performance.now()for precise measurements, notDate.now(). - Avoid forced reflows — don't interleave style reads and writes.
- Use code splitting — load only what's needed, especially for large applications.
- Monitor memory usage — track heap size, look for detached DOM nodes, investigate growing memory.
8. Challenge Assignment (Optional)
"Real-Time Performance Profiler" Challenge
Build a real-time performance profiling tool that can be used to monitor any web application.
Requirements:
- FPS Meter — Overlay on the page showing current, average, min, and max FPS.
- Memory Gauge — Display current heap usage and detect leaks by monitoring growth over time.
- Long Task Detector — Use
PerformanceObserverto detect tasks longer than 50ms (which cause noticeable jank). - Layout Thrash Detector — Wrap
getComputedStyle,offsetHeight,offsetWidth, etc. to log when they trigger forced reflows. - Recommendation Engine — Based on detected issues, suggest fixes (e.g., "Debounce scroll handler", "Use Web Worker for computation").
Bonus:
- Add a flame chart visualization using Canvas
- Allow exporting profiling data as JSON
- Add alerts when memory grows by more than 10MB in 30 seconds
- Support remote profiling via WebSocket
Example usage:
const profiler = new Profiler();
profiler.start();
// ... user interacts with the app ...
profiler.stop();
console.log(profiler.getReport());
// {
// fps: { current: 58, average: 60, min: 12, max: 61 },
// memory: { current: 45.2, growth: 2.1, leakDetected: false },
// longTasks: [{ duration: 120, timestamp: "..." }],
// layoutThrashes: [{ property: "offsetHeight", count: 15 }],
// recommendations: [
// "Long task detected (120ms). Consider deferring or using Web Worker.",
// "Layout thrash detected on offsetHeight (15 calls). Batch reads and writes."
// ]
// }
9. Knowledge Check (Quiz)
What is the main difference between the stack and the heap?
- a) Stack stores objects; heap stores primitives
- b) Stack is for primitive values and call frames; heap stores objects
- c) Stack is slower than heap
- d) There is no difference
Which of the following causes a memory leak?
- a) Using
letinstead ofvar - b) A
setIntervalthat references DOM elements that have been removed - c) Using arrow functions
- d) Calling
JSON.parse()on a large string
- a) Using
What is a forced reflow (layout thrashing)?
- a) The browser recalculating layout because you read a style property after making changes
- b) The browser crashing due to an infinite loop
- c) A CSS animation running at 60fps
- d) The page scrolling automatically
How do you minimize reflows when adding many elements to the DOM?
- a) Add them one by one with
appendChild - b) Use
innerHTMLwith a single string - c) Use a
DocumentFragmentand append it once - d) Both b and c are correct
- a) Add them one by one with
What does
requestAnimationFramedo?- a) Runs a function as fast as possible
- b) Runs a function before the next repaint, synced with the browser's refresh rate
- c) Runs a function after a specified delay
- d) Runs a function in a separate thread
How can you offload heavy computation without blocking the UI?
- a) Use
setTimeoutwith a delay of 0 - b) Use a Web Worker
- c) Use
requestAnimationFrame - d) All of the above can help, but Web Workers are best for CPU-heavy tasks
- a) Use
What is the purpose of
performance.now()?- a) To get the current date and time
- b) To get a high-resolution timestamp for measuring performance
- c) To check the browser's performance score
- d) To optimize code automatically
Which tool is best for finding memory leaks in a web application?
- a)
console.log() - b) Chrome DevTools Performance tab
- c) Chrome DevTools Memory tab (heap snapshots)
- d) A text editor
- a)
Answers: 1-b, 2-b, 3-a, 4-d (fragment or innerHTML both reduce reflows), 5-b, 6-d, 7-b, 8-c
10. Additional Resources
- MDN: Memory Management → developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
- MDN: Performance API → developer.mozilla.org/en-US/docs/Web/API/Performance
- MDN: Web Workers → developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
- MDN: Avoiding forced reflows → developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Tips/Using_the_DOM
- Chrome DevTools: Memory Issues → developer.chrome.com/docs/devtools/memory-problems/
- Chrome DevTools: Performance → developer.chrome.com/docs/devtools/performance/
- JavaScript.info: Garbage Collection → javascript.info/garbage-collection
- JavaScript.info: Performance → javascript.info/performance
- Video: "JavaScript Memory Leaks" by Web Dev Simplified
- Video: "Performance Optimization Techniques" by Google Chrome DevTools
- Book: Chapter 22 of Eloquent JavaScript — "Performance and Optimization" (online version)
11. Summary
Today you learned:
- Memory management — stack vs heap, mark-and-sweep garbage collection, reference counting
- Common memory leaks — global variables, forgotten timers, detached DOM nodes, closures, event listeners
- DOM optimization — minimizing reflows/repaints, using DocumentFragment, batch reads and writes, avoiding layout thrashing
- Performance measurement —
console.time(),performance.now(),performance.mark()/measure(), Chrome DevTools Performance/Memory tabs - Debouncing and throttling — limiting expensive event handlers
- Web Workers — offloading heavy computation to separate threads without blocking the UI
- Performance profiling — FPS monitoring, memory tracking, long task detection, layout thrash detection
- Practical application — Performance Dashboard with monitoring, measurement, and recommendations
You now have the knowledge to write efficient, memory-safe JavaScript and diagnose performance issues. Tomorrow we'll explore design patterns — reusable solutions to common programming problems.
"Performance is not about premature optimization — it's about writing code that respects the user's time and device resources. Measure first, optimize second."

Comments
Post a Comment