Stop worrying about the potholes in the road and enjoy the journey

Efficient async Fibonacci Calculation in JavaScript: Node.js and Browser Approach

The Fibonacci sequence is widely used in programming, and calculating it is a great way to understand algorithms, asynchronous programming, and optimization. In this article, we’ll look at how to calculate Fibonacci numbers in JavaScript for Node.js asynchronously and adapt this approach for browsers. We’ll cover how to use caching to speed up the calculations and why an asynchronous approach with Promise and setImmediate is well-suited for large numbers. We’ll start with a Node.js implementation and then move to a browser-friendly version.

1. A Quick Introduction to Fibonacci Numbers

Fibonacci numbers form a sequence where each number is the sum of the two preceding ones. The sequence starts with 0 and 1:

0, 1, 1, 2, 3, 5, 8, 13, 21, ...

Mathematically, it’s defined as:

F(0) = 0, F(1) = 1

F(n) = F(n-1) + F(n-2) for n ≥ 2

For small values, such as n = 10, this calculation is manageable with simple recursion. But if we need to calculate, say, F(1000), a basic recursive algorithm becomes very slow. To make this efficient, we need to use caching and asynchronous functions.

2. Fibonacci Implementation for Node.js

Using Promise, setImmediate, and Caching

Node.js provides setImmediate, which allows code execution to be deferred until the next event loop iteration, without blocking the main thread. This is particularly helpful for recursive tasks like calculating Fibonacci numbers, as it prevents stack overflow and lets us use Promise.

Here’s an implementation for Node.js:

const cache = new Map();

function fib(n) {
    return new Promise((resolve, reject) => {
        if (n === 0 || n === 1) {
            return resolve(n); // Base cases: F(0) = 0, F(1) = 1
        }

        if (cache.has(n)) {
            return resolve(cache.get(n)); // Return from cache if already calculated
        }

        setImmediate(() => { // Defer execution to avoid blocking
            fib(n - 1).then(fib1 => fib(n - 2).then(fib2 => {
                cache.set(n, fib1 + fib2); // Cache result for F(n)
                resolve(fib1 + fib2);
            }));

            /*
             * a lot of time taking
             Promise.all([fib(n - 1), fib(n - 2)]).then(([fib1, fib2]) => {
             cache.set(n, fib1 + fib2);
             resolve(fib1 + fib2);
             });
             * 
             */

        });
    });
}

fib(1000).then(res => console.log(`F(1000) = ${res}, peromance time: ${performance.now().toFixed(2)}`));

How This Function Works:

  • Caching: We use a Map object as a cache to store calculated Fibonacci numbers. When the fib function is called with a new n, it first checks the cache and returns the value if it’s already stored.
  • Asynchronous setImmediate: setImmediate allows us to offload each recursive call without overflowing the stack. It adds execution to the macrotask queue, letting JavaScript handle other tasks in the meantime.
  • Why It’s Fast: This method is highly efficient as caching prevents redundant calculations, and asynchronous execution avoids blocking.

Result: fib(1000) returns almost instantly by leveraging cached values and processing each recursive step asynchronously.

3. Fibonacci Implementation for Browsers

Browsers don’t support setImmediate, but we can achieve similar behavior using setTimeout with a 0 delay. This lets the function yield control to other tasks in the event queue, similar to setImmediate in Node.js.

Code for Browsers

const cache = new Map();

function fib(n) {
    return new Promise((resolve, reject) => {
        if (n === 0 || n === 1) {
            return resolve(n);
        }

        if (cache.has(n)) {
            return resolve(cache.get(n));
        }

        // Use setTimeout instead of setImmediate for async execution
        setTimeout(() => {
            fib(n - 1).then(fib1 => fib(n - 2).then(fib2 => {
                cache.set(n, fib1 + fib2);
                resolve(fib1 + fib2);
            }));
        }, 0); // 0-delay to avoid blocking the main thread
    });
}

fib(1000).then(result => console.log(`F(1000) = ${result}`));

How It Works in the Browser:

  • Caching: As in Node.js, the browser version uses Map for caching, preventing duplicate calculations.
  • Asynchronous setTimeout: Instead of setImmediate, we use setTimeout with a delay of 0. This defers the execution and lets JavaScript handle other tasks in the event loop, avoiding thread blocking.
  • Performance Benefits: While setTimeout has a bit more overhead than setImmediate, this approach is still quick and effective for browser-based calculations.

4. Why Promise.all Can Slow Things Down

If we replace the current implementation with Promise.all:

Promise.all([fib(n - 1), fib(n - 2)]).then(([fib1, fib2]) => {
    cache.set(n, fib1 + fib2);
    resolve(fib1 + fib2);
});

we’ll see a performance drop. The reason is that Promise.all starts both recursive calls simultaneously. If a value isn’t cached yet, this can lead to a large number of parallel recursive calls, putting a heavy load on the event loop. The current implementation (with sequential fib(n - 1) and fib(n - 2)) is more efficient for cached calculations.

5. Conclusion

To calculate Fibonacci numbers in JavaScript, an efficient approach includes:

  • Caching: Storing previous results to speed up calculations for large n.
  • Asynchronous Execution: Using setImmediate in Node.js and setTimeout with 0 delay in browsers to avoid blocking the main thread.
  • Optimized Sequence: Launching recursive calls sequentially rather than in parallel leverages caching more effectively.

These methods make it possible to calculate even large values, like fib(1000), almost instantly.

2