How to optimize JavaScript async functions for performance?

Content verified by Anycode AI
July 21, 2024
One of the most important issues of modern web development has to do with how to properly handle asynchronous operations—that is, creating an application efficiently with regard to performance and user experience. JavaScript async functions make it possible for nonblocking execution of code, but improper usage may result in performance bottlenecks, excessive network requests, or unhandled errors. The detailed guide that follows is devoted to showing how to optimize async functions: parallelize independent promises, limit the number of concurrent requests, cache the results, and graceful error handling. Following these best practices can help developers boost the performance and reliability of their application executions, ensure faster loading, and give users a smoother experience.

Optimizing JavaScript Async Functions

Optimizing JavaScript async functions is key to boosting the performance of your web apps. Let me walk you through some tips and tricks, and make them sound less like a robot, more like a human!

Understanding Async Functions

Async functions let you write promise-based code that looks and feels like regular synchronous code. It's all thanks to the async and await syntax. This makes your code easier to read and handle errors compared to older promise chains or callbacks.

async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
}

Performance Optimization Techniques

Avoid Sequential await Calls

Sequential await calls might be straightforward but they can slow things down by making operations wait for each other without reason.

// Bad Example (Sequential Execution, don't do this!)
async function getUserData() {
    const userDetails = await fetchUserDetails();
    const userPosts = await fetchUserPosts();
    const userComments = await fetchUserComments();
    return { userDetails, userPosts, userComments };
}

// Good Example (Parallel Execution, much better!)
async function getUserData() {
    const [userDetails, userPosts, userComments] = await Promise.all([
        fetchUserDetails(),
        fetchUserPosts(),
        fetchUserComments()
    ]);
    return { userDetails, userPosts, userComments };
}

Using Promise.all lets you run multiple async tasks side-by-side, which can shave off significant time.

Use Promise.allSettled for Independent Promises

When tasks don’t rely on each other and their failures shouldn’t interfere, Promise.allSettled is your friend.

async function fetchMultipleData() {
    const results = await Promise.allSettled([
        fetchUserDetails(),
        fetchUserPosts(),
        fetchUserComments()
    ]);

    results.forEach(result => {
        if (result.status === 'fulfilled') {
            console.log(result.value);
        } else {
            console.error(result.reason);
        }
    });
}

Leverage Promise.any for Fastest Resolved Promise

Need the quickest result from a bunch of promises? Promise.any is the answer! It's handy when querying multiple sources for the same type of data.

async function fetchFastestData() {
    const data = await Promise.any([
        fetch('https://api.example.com/endpoint1'),
        fetch('https://api.backup.com/endpoint1'),
        fetch('https://api.cache.com/endpoint1')
    ]);

    return data.json();
}

Cache Repeated Data Fetches

To cut down on network requests for frequently-accessed and rarely-changed data, the good old cache is your pal.

const cache = new Map();

async function fetchDataWithCache(url) {
    if (cache.has(url)) {
        return cache.get(url);
    }
    
    const response = await fetch(url);
    const data = await response.json();
    cache.set(url, data);
    return data;
}

Use Web Workers

For heavy computations that would otherwise block the main thread, Web Workers are amazing. They do the heavy lifting so your main thread can stay responsive.

// Main Thread Code
const worker = new Worker('worker.js');

worker.postMessage({ type: 'start', payload: largeDataSet });

worker.onmessage = function(event) {
    if (event.data.type === 'result') {
        console.log('Processed data:', event.data.payload);
    }
};

// Inside worker.js
onmessage = function(event) {
    if (event.data.type === 'start') {
        const processedData = computeIntensiveTask(event.data.payload);
        postMessage({ type: 'result', payload: processedData });
    }
};

function computeIntensiveTask(data) {
    // Perform heavy computations here   
    return processedResult; 
}

Debounce Network Requests

With inputs triggering network requests, like search bars, debouncing can save you from sending too many requests too quickly.

function debounce(func, wait) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}

const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(async (event) => {
    const query = event.target.value;
    const response = await fetch(`/search?q=${query}`);
    const results = await response.json();
    displayResults(results);
}, 300));

Throttle Network Requests

Similar to debouncing, throttling makes sure a function only runs at certain intervals. It’s useful for managing regular actions like scroll events or button clicks.

function throttle(func, limit) {
    let lastFunc;
    let lastRan;
    return function(...args) {
        const context = this;
        
        if (!lastRan) {
            func.apply(context, args);
            lastRan = Date.now();
        } else {
            clearTimeout(lastFunc);
            lastFunc = setTimeout(() => {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    };
}

const button = document.getElementById('loadMoreButton');
button.addEventListener('click', throttle(async () => {
    const response = await fetch('/loadMoreData');
    const newData = await response.json();
    displayData(newData);
}, 2000));
Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode