Skillversitas

53. JavaScript Performance: Code Optimization

Introduction

Performance optimization is the art of making JavaScript code run faster and more efficiently in memory usage. Techniques such as debouncing, throttling, memoization, and lazy loading help keep the application responsive even when processing large amounts of data.

For IT students building a campus portal with thousands of student records, good performance is critical. A slow search or a laggy page will make the user experience bad.

Why Is Performance Important?

A modern campus portal has to process thousands of student records, class schedules, and grades in real time. Without optimization, the search input can lag, pages load slowly, and API calls take a long time. The user experience is directly affected.

Debouncing

// Debounce: delay execution until the user stops typing
function debounce(func, delay) {
    let timeoutId;

    return function(...args) {
        clearTimeout(timeoutId);

        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Usage: Search input
const searchInput = document.getElementById('search');
const resultsDiv = document.getElementById('results');

async function searchStudents(query) {
    if (!query) {
        resultsDiv.innerHTML = '';
        return;
    }

    resultsDiv.innerHTML = '<p>Searching...</p>';

    try {
        const response = await fetch(`/api/students/search?q=${encodeURIComponent(query)}`);
        const students = await response.json();

        resultsDiv.innerHTML = students.map(s => `
            <div class="student-card">
                <h4>${s.name}</h4>
                <p>${s.studentId} - ${s.majorCode}</p>
            </div>
        `).join('');
    } catch (error) {
        resultsDiv.innerHTML = '<p>Search failed</p>';
    }
}

// Without debounce: the API is called on every keystroke (wasteful!)
// searchInput.addEventListener('input', (e) => searchStudents(e.target.value));

// With debounce: the API is called 300ms after the user stops typing
const debouncedSearch = debounce((e) => searchStudents(e.target.value), 300);
searchInput.addEventListener('input', debouncedSearch);
// Debounce with immediate option
function debounceImmediate(func, delay) {
    let timeoutId;
    let lastArgs;

    return function(...args) {
        lastArgs = args;

        if (!timeoutId) {
            func.apply(this, args);
        }

        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            timeoutId = null;
        }, delay);
    };
}

// Immediate: the first call runs right away, subsequent calls are debounced
const immediateSearch = debounceImmediate((query) => {
    console.log('Searching:', query);
}, 300);

Throttling

// Throttle: limit execution frequency
function throttle(func, limit) {
    let inThrottle = false;
    let lastArgs;
    let lastContext;

    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;

            setTimeout(() => {
                inThrottle = false;
                if (lastArgs) {
                    func.apply(lastContext, lastArgs);
                    lastArgs = null;
                    lastContext = null;
                }
            }, limit);
        } else {
            lastArgs = args;
            lastContext = this;
        }
    };
}

// Usage: Scroll handler
function handleScroll() {
    console.log('Scroll position:', window.scrollY);
    // Load more data when near bottom
    if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
        loadMoreStudents();
    }
}

// Without throttle: the handler is called hundreds of times per second while scrolling
// window.addEventListener('scroll', handleScroll);

// With throttle: the handler is called at most once every 200ms
window.addEventListener('scroll', throttle(handleScroll, 200));

// Throttle for resize
window.addEventListener('resize', throttle(() => {
    console.log('Window resized:', window.innerWidth, window.innerHeight);
    recalculateLayout();
}, 250));

Memoization

// Memoization: cache the results of expensive computations
function memoize(fn) {
    const cache = new Map();

    return function(...args) {
        const key = JSON.stringify(args);

        if (cache.has(key)) {
            console.log('Cache hit:', key);
            return cache.get(key);
        }

        console.log('Computing:', key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// Usage: compute GPA (expensive computation)
function calculateGPA(gradeList) {
    console.log('Calculating GPA...');
    // Simulate a slow process
    const total = gradeList.reduce((sum, n) => sum + n, 0);
    return total / gradeList.length;
}

const memoizedGPA = memoize(calculateGPA);

const grade1 = [85, 90, 88, 92, 87];
const grade2 = [78, 82, 85, 80, 79];

console.log(memoizedGPA(grade1)); // Computing: [85, 90...] -> 88.4
console.log(memoizedGPA(grade1)); // Cache hit: [85, 90...] -> 88.4
console.log(memoizedGPA(grade2)); // Computing: [78, 82...] -> 80.8
// Memoize with max cache size and TTL
function memoizeAdvanced(fn, options = {}) {
    const { maxSize = 100, ttl = 60000 } = options;
    const cache = new Map();
    const timestamps = new Map();

    function getCacheKey(args) {
        return args.map(arg => {
            if (typeof arg === 'object' && arg !== null) {
                return JSON.stringify(arg);
            }
            return String(arg);
        }).join('::');
    }

    return function(...args) {
        const key = getCacheKey(args);

        // Check if cached and not expired
        if (cache.has(key)) {
            const timestamp = timestamps.get(key);
            if (Date.now() - timestamp < ttl) {
                return cache.get(key);
            }
            cache.delete(key);
            timestamps.delete(key);
        }

        // Evict oldest if cache full
        if (cache.size >= maxSize) {
            const oldestKey = timestamps.keys().next().value;
            cache.delete(oldestKey);
            timestamps.delete(oldestKey);
        }

        const result = fn.apply(this, args);
        cache.set(key, result);
        timestamps.set(key, Date.now());

        return result;
    };
}

// Usage
const fetchStudent = memoizeAdvanced(
    async (studentId) => {
        const res = await fetch(`/api/students/${studentId}`);
        return res.json();
    },
    { maxSize: 50, ttl: 5 * 60 * 1000 } // Cache 50 items, expire after 5 minutes
);

Lazy Loading

// Lazy load images
function initLazyImages() {
    const images = document.querySelectorAll('img[data-src]');

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });

    images.forEach(img => observer.observe(img));
}

// Lazy load modules
async function loadModule(modulePath) {
    const module = await import(modulePath);
    return module.default || module;
}

// Lazy load components on scroll
function initScrollLoader() {
    const loader = document.createElement('div');
    loader.id = 'scroll-loader';
    document.body.appendChild(loader);

    const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
            loadMoreContent();
        }
    });

    observer.observe(loader);
}

// Lazy load with delay
function lazyLoadWithDelay(fn, delay = 200) {
    let timer;

    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

Code Splitting

// Dynamic import for code splitting
async function loadCourseModule() {
    const { CourseManager } = await import('./modules/CourseManager.js');
    return new CourseManager();
}

async function loadGradeModule() {
    const { GradeCalculator } = await import('./modules/GradeCalculator.js');
    return new GradeCalculator();
}

// Route-based splitting
const routes = {
    '/dashboard': () => import('./pages/Dashboard.js'),
    '/courses': () => import('./pages/Courses.js'),
    '/grades': () => import('./pages/Grades.js'),
    '/profile': () => import('./pages/Profile.js')
};

async function navigateTo(path) {
    const loader = routes[path];
    if (loader) {
        const module = await loader();
        module.render();
    }
}

// Preload modules
function preloadModules(paths) {
    paths.forEach(path => {
        const link = document.createElement('link');
        link.rel = 'modulepreload';
        link.href = path;
        document.head.appendChild(link);
    });
}

preloadModules([
    './modules/CourseManager.js',
    './pages/Grades.js'
]);

requestIdleCallback

// requestIdleCallback: run tasks when the browser is idle
function processInBackground(tasks) {
    let index = 0;

    function work(deadline) {
        // Process tasks while there is still time
        while (index < tasks.length && deadline.timeRemaining() > 0) {
            tasks[index]();
            index++;
        }

        if (index < tasks.length) {
            requestIdleCallback(work);
        }
    }

    requestIdleCallback(work);
}

// Usage: render student data without blocking the UI
const studentData = Array.from({ length: 1000 }, (_, i) => ({
    studentId: String(i + 1).padStart(10, '0'),
    name: `Student ${i + 1}`,
    majorCode: ['CS', 'IS', 'CE'][i % 3],
    gpa: (Math.random() * 2 + 2).toFixed(2)
}));

const tasks = studentData.map(student => () => {
    const card = document.createElement('div');
    card.className = 'student-card';
    card.innerHTML = `<h4>${student.name}</h4><p>${student.studentId}</p>`;
    document.getElementById('student-grid').appendChild(card);
});

processInBackground(tasks);

// Fallback if the browser doesn't support it
if (!('requestIdleCallback' in window)) {
    window.requestIdleCallback = (cb) => setTimeout(cb, 1);
    window.cancelIdleCallback = clearTimeout;
}

Performance Monitoring

// performance.now(): precise timing
function measurePerformance(fn) {
    return function(...args) {
        const start = performance.now();
        const result = fn.apply(this, args);
        const end = performance.now();
        console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
        return result;
    };
}

// Performance marks
function trackOperation(name, fn) {
    performance.mark(`${name}-start`);
    const result = fn();
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);

    const measure = performance.getEntriesByName(name)[0];
    console.log(`${name}: ${measure.duration.toFixed(2)}ms`);

    return result;
}

// Usage example
const sortedStudents = trackOperation('Sort Students', () => {
    return studentData.sort((a, b) => a.name.localeCompare(b.name));
});

// Performance Observer
if ('PerformanceObserver' in window) {
    const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
            console.log(`Performance: ${entry.name} - ${entry.duration.toFixed(2)}ms`);
        }
    });

    observer.observe({ entryTypes: ['measure', 'navigation'] });
}
class OptimizedStudentSearch {
    #searchInput;
    #resultsContainer;
    #debouncedSearch;
    #cache;
    #abortController;
    #currentPage;
    #isLoading;

    constructor(inputId, containerId) {
        this.#searchInput = document.getElementById(inputId);
        this.#resultsContainer = document.getElementById(containerId);
        this.#cache = new Map();
        this.#currentPage = 1;
        this.#isLoading = false;

        // Debounced search: 300ms delay
        this.#debouncedSearch = this.#debounce(this.#search.bind(this), 300);

        this.#init();
    }

    #init() {
        this.#searchInput.addEventListener('input', (e) => {
            this.#currentPage = 1;
            this.#debouncedSearch(e.target.value);
        });

        this.#searchInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                this.#debouncedSearch.cancel();
                this.#search(e.target.value);
            }
        });
    }

    #debounce(func, delay) {
        let timer;

        const debounced = (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => func(...args), delay);
        };

        debounced.cancel = () => clearTimeout(timer);

        return debounced;
    }

    async #search(query) {
        if (!query || query.trim().length < 2) {
            this.#resultsContainer.innerHTML = '';
            return;
        }

        // Check cache
        const cacheKey = `${query}-${this.#currentPage}`;
        if (this.#cache.has(cacheKey)) {
            this.#renderResults(this.#cache.get(cacheKey));
            return;
        }

        // Abort previous request
        if (this.#abortController) {
            this.#abortController.abort();
        }
        this.#abortController = new AbortController();

        this.#isLoading = true;
        this.#showLoading();

        try {
            const response = await fetch(
                `/api/students/search?q=${encodeURIComponent(query)}&page=${this.#currentPage}`,
                { signal: this.#abortController.signal }
            );

            const data = await response.json();

            // Cache result
            this.#cache.set(cacheKey, data);

            // Limit cache size
            if (this.#cache.size > 50) {
                const firstKey = this.#cache.keys().next().value;
                this.#cache.delete(firstKey);
            }

            this.#renderResults(data);
        } catch (error) {
            if (error.name !== 'AbortError') {
                this.#resultsContainer.innerHTML = '<p class="error">Search failed</p>';
            }
        } finally {
            this.#isLoading = false;
        }
    }

    #showLoading() {
        this.#resultsContainer.innerHTML = `
            <div class="loading">
                <div class="spinner"></div>
                <p>Searching...</p>
            </div>
        `;
    }

    #renderResults(data) {
        const { students, total, page, totalPages } = data;

        if (students.length === 0) {
            this.#resultsContainer.innerHTML = '<p class="no-results">No results found</p>';
            return;
        }

        this.#resultsContainer.innerHTML = `
            <div class="search-info">
                <p>Found ${total} students</p>
            </div>
            <div class="student-list">
                ${students.map(s => `
                    <div class="student-card">
                        <h4>${this.#highlight(s.name, this.#searchInput.value)}</h4>
                        <p>${s.studentId} - ${s.majorCode}</p>
                        <p>GPA: ${s.gpa}</p>
                    </div>
                `).join('')}
            </div>
            <div class="pagination">
                <button ${page <= 1 ? 'disabled' : ''} onclick="searchApp.prevPage()">Prev</button>
                <span>Page ${page} of ${totalPages}</span>
                <button ${page >= totalPages ? 'disabled' : ''} onclick="searchApp.nextPage()">Next</button>
            </div>
        `;
    }

    #highlight(text, query) {
        if (!query) return text;
        const regex = new RegExp(`(${query})`, 'gi');
        return text.replace(regex, '<mark>$1</mark>');
    }

    nextPage() {
        this.#currentPage++;
        this.#search(this.#searchInput.value);
    }

    prevPage() {
        if (this.#currentPage > 1) {
            this.#currentPage--;
            this.#search(this.#searchInput.value);
        }
    }
}

// Initialization
const searchApp = new OptimizedStudentSearch('search-input', 'search-results');

Example: Virtual Scrolling for a Student List

class VirtualList {
    #container;
    #items;
    #itemHeight;
    #visibleItems;
    #scrollTop;
    #startIndex;
    #renderFn;

    constructor(containerId, items, options = {}) {
        this.#container = document.getElementById(containerId);
        this.#items = items;
        this.#itemHeight = options.itemHeight || 50;
        this.#visibleItems = options.visibleItems || 20;
        this.#renderFn = options.renderFn || ((item) => `<div>${JSON.stringify(item)}</div>`);

        this.#init();
    }

    #init() {
        // Create wrapper
        const wrapper = document.createElement('div');
        wrapper.style.height = `${this.#visibleItems * this.#itemHeight}px`;
        wrapper.style.overflow = 'auto';
        wrapper.style.position = 'relative';

        // Create scroll content (for the scrollbar)
        const scrollContent = document.createElement('div');
        scrollContent.style.height = `${this.#items.length * this.#itemHeight}px`;

        // Create visible area
        const visibleContent = document.createElement('div');
        visibleContent.style.position = 'absolute';
        visibleContent.style.width = '100%';

        wrapper.appendChild(scrollContent);
        wrapper.appendChild(visibleContent);
        this.#container.appendChild(wrapper);

        // Throttled scroll handler
        wrapper.addEventListener('scroll', this.#throttle(() => {
            this.#updateVisibleItems(wrapper, visibleContent);
        }, 16)); // ~60fps

        // Initial render
        this.#updateVisibleItems(wrapper, visibleContent);
    }

    #updateVisibleItems(wrapper, visibleContent) {
        const scrollTop = wrapper.scrollTop;
        this.#startIndex = Math.floor(scrollTop / this.#itemHeight);
        const endIndex = Math.min(
            this.#startIndex + this.#visibleItems + 2,
            this.#items.length
        );

        visibleContent.style.transform = `translateY(${this.#startIndex * this.#itemHeight}px)`;

        const visibleSlice = this.#items.slice(this.#startIndex, endIndex);
        visibleContent.innerHTML = visibleSlice
            .map((item, i) => `
                <div style="height: ${this.#itemHeight}px; display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid #eee;">
                    ${this.#renderFn(item, this.#startIndex + i)}
                </div>
            `).join('');
    }

    #throttle(fn, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                fn.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }

    updateItems(newItems) {
        this.#items = newItems;
    }
}

// Usage
const allStudents = Array.from({ length: 10000 }, (_, i) => ({
    studentId: String(i + 1).padStart(10, '0'),
    name: `Student ${i + 1}`,
    majorCode: ['CS', 'IS', 'CE'][i % 3]
}));

new VirtualList('student-list', allStudents, {
    itemHeight: 50,
    visibleItems: 20,
    renderFn: (student) => `
        <div style="flex: 1;">
            <strong>${student.name}</strong>
            <span style="margin-left: 16px; color: #666;">${student.studentId}</span>
            <span style="margin-left: 16px;">${student.majorCode}</span>
        </div>
    `
});

Code Explanation

Debouncing

  • Delays execution until the user stops interacting
  • Suited for search inputs, resize handlers, form validation
  • Parameter: delay (ms) that determines the wait time

Throttling

  • Limits execution frequency to once per period
  • Suited for scroll handlers, mousemove, real-time updates
  • Parameter: limit (ms) that determines the minimum interval

Memoization

  • Caches computation results based on input
  • Suited for pure functions with limited inputs
  • Consider cache eviction (LRU, TTL)

Lazy Loading

  • Load resources only when needed
  • Use IntersectionObserver for visibility-based loading
  • Dynamic import() for code splitting

Performance Monitoring

  • performance.now() for precise timing
  • performance.mark() and performance.measure() for benchmarking
  • PerformanceObserver for real-time monitoring

Conclusion

Performance optimization is an important part of web development. For a campus portal that processes thousands of records, use debouncing for search inputs, throttling for scroll handlers, memoization for repeated computations, and lazy loading for large resources. Virtual scrolling helps display 10,000+ records without lag. Always monitor performance with the performance API and fix the bottlenecks you find.