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:

AreaStoresSpeedSize
StackPrimitive values, function call frames, variable referencesFastSmall (limited)
HeapObjects, arrays, functions, closuresSlowerLarge (limited by system)

How Garbage Collection Works

The most common algorithm is mark-and-sweep:

  1. Mark — Starting from "roots" (global object, current function scope, DOM elements), the GC marks all reachable objects.
  2. 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

TechniqueDescriptionUse Case
DebounceDelays execution until after a pause in callsSearch input, window resize
ThrottleLimits execution to at most once per intervalScroll 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

OperationWhat HappensCost
ReflowBrowser recalculates layout positions/sizesExpensive
RepaintBrowser 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, or document
  • 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

  1. Use console.time() and console.timeEnd() to measure how long it takes to sum numbers from 1 to 1,000,000 with a for loop.
  2. Create a memory leak by creating a setInterval that never gets cleared. Identify it in Chrome DevTools Memory tab.
  3. Use performance.now() to measure the execution time of a function that creates an array of 1,000,000 zeros.

Intermediate

  1. 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 with console.time.
  2. Create a debounced search function that only sends a fetch request after the user has stopped typing for 300ms.
  3. Use Chrome DevTools Performance tab to record and analyze a page with layout thrashing. Fix the issue and compare before/after.

Advanced

  1. 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
  2. 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

MistakeWhy it's wrongCorrect approach
Creating objects inside hot loopsGenerates garbage, triggering GC more oftenCreate objects outside loops or reuse them
Reading layout properties after DOM mutationsForces synchronous reflow, hurting performanceBatch reads and writes separately
Using innerHTML for complex updatesParses HTML, destroys and recreates all child nodesUse createElement and fragments for complex updates
Forgetting to clean up timers and listenersCauses memory leaks and zombie callbacksAlways pair setInterval with clearInterval, addEventListener with removeEventListener
Using too many event listenersEach listener uses memory; hundreds slow down interactionUse event delegation for similar elements
Storing large data in closures unnecessarilyPrevents GC from collecting that dataNullify references when no longer needed or restructure code
Not using Web Workers for heavy computationBlocks the main thread, freezing the UIOffload CPU-intensive work to Web Workers
Over-optimizing prematurelyWastes development time on code that doesn't need optimizationProfile 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 requestAnimationFrame for 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, not Date.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:

  1. FPS Meter — Overlay on the page showing current, average, min, and max FPS.
  2. Memory Gauge — Display current heap usage and detect leaks by monitoring growth over time.
  3. Long Task Detector — Use PerformanceObserver to detect tasks longer than 50ms (which cause noticeable jank).
  4. Layout Thrash Detector — Wrap getComputedStyle, offsetHeight, offsetWidth, etc. to log when they trigger forced reflows.
  5. 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)

  1. 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
  2. Which of the following causes a memory leak?

    • a) Using let instead of var
    • b) A setInterval that references DOM elements that have been removed
    • c) Using arrow functions
    • d) Calling JSON.parse() on a large string
  3. 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
  4. How do you minimize reflows when adding many elements to the DOM?

    • a) Add them one by one with appendChild
    • b) Use innerHTML with a single string
    • c) Use a DocumentFragment and append it once
    • d) Both b and c are correct
  5. What does requestAnimationFrame do?

    • 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
  6. How can you offload heavy computation without blocking the UI?

    • a) Use setTimeout with 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
  7. 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
  8. 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

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


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 measurementconsole.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

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals