Advance Javascript : Lazy Loading

In my previous articles I explained optimization patterns such as deboucing and throttling, in this article let’s do a deep dive into lazy loading , which defers the loading of resources until they are actually needed. This article will walk you through a simple example of implementing lazy loading in a TypeScript web application.

What is Lazy Loading?

Lazy loading is a design pattern commonly used to delay the initialization of objects until the point at which they are needed. In the context of web development, this often refers to loading resources like images, scripts, or modules only when they are required, rather than at the start of the application. This can significantly reduce initial load times and improve overall performance.

Project Overview

We will create a web page with a button. When the button is clicked, a module containing a heavy computation function will be loaded and executed. This example uses TypeScript and native ES modules to achieve lazy loading.

Setting Up the Project

  • Project Structure
lazy-loading-example/
├── dist/
│   ├── index.html
│   ├── app.js
│   └── lazyModule.js
├── src/
│   ├── index.html
│   ├── app.ts
│   └── lazyModule.ts
├── tsconfig.json
└── package.json
  • Initialize the Project
npm install typescript --save-dev
npx tsc --init
  • Configure TypeScript

Edit ‘tsconfig.json ‘to target ES6 and use ESNext modules:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Writing the Code

  • Create HTML File

Create ‘src/index.html‘ with a button and script tag

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Lazy Loading Example</title>
</head>
<body>
  <h1>Lazy Loading Example</h1>
  <button id="loadButton">Load Module</button>
  <script type="module" src="./app.js"></script>
</body>
</html>
  • Create Lazy Module

Create ‘src/lazyModule.ts‘ which contains a heavy task function:

export const performHeavyTask = () => {
  console.log('Performing a heavy task...');
  // Simulate a heavy task
  for (let i = 0; i < 1e7; i++) {}
  console.log('Heavy task completed.');
};
  • Create Main Application File

Create ‘src/app.ts‘ to handle button click and load the module

const loadButton = document.getElementById('loadButton') as HTMLButtonElement;

loadButton.addEventListener('click', async () => {
  const module = await import('./lazyModule.js');
  module.performHeavyTask();
});

Compile and Run

  • Compile the TypeScript files
npx tsc
  • Serve the Application

Use a static file server to serve the files. Here’s a quick way to do it using ‘http-server

npm install -g http-server
http-server dist
  • Open in Browser

Open your browser and navigate to http://localhost:8080 (or the port http-server provides). Click the “Load Module” button to see the lazy loading in action. The performHeavyTask function from lazyModule will be dynamically imported and executed, demonstrating lazy loading.

Conclusion

Lazy loading is an effective way to optimize web application performance by loading resources only when they are needed. In this example, we’ve seen how to implement lazy loading in a TypeScript web application using native ES modules. This technique can be extended to other resources like images and scripts to further enhance your application’s performance.

By adopting lazy loading, you can ensure that your web applications are both efficient and responsive, providing a better experience for your users

For complete source – https://github.com/IndikaMaligaspe/advance-javascrips/tree/master/topics/performance-optimization

Advance Javascript: Throttling

In my previous article I went through a performance optimization strategy called debouncing, in this article let’s go through an effective technique to manage the frequency of function execution – throttling. We’ll explore what throttling is, why it’s useful, and how to implement it with examples from both a console application and a web application using TypeScript.

What is Throttling?

Throttling is a technique used to ensure that a function is executed at most once in a specified period. This is particularly useful in scenarios where an event fires multiple times in quick succession, like keystrokes, window resizing, or scroll events. By throttling, we can control the execution frequency of a function, improving performance and user experience.

Why Use Throttling?

Imagine a scenario where you have a function that updates the UI based on user scroll events. Without throttling, the function could be called numerous times per second, potentially degrading performance. Throttling ensures that the function is called at most once every specified period, reducing the workload and improving efficiency.

Throttling in a Console Application

Let’s start with a simple example in a console application using Node.js. We’ll create a throttled function that logs a message only once every 2 seconds while the user types.

TypeScript Example:

import readline from 'readline';

type Throttle = <T extends(...args:any[]) => void> (func:T, limit: number) => (...args: Parameters<T>) => void;

const throttle : Throttle = (func, limit) => {
    let lastFunc: NodeJS.Timeout | undefined;
    let lastRan: number;

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

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const handleKeystroke = throttle((input: string) => {
    console.log('Keystroke is :', input.trim())
}, 2000);

console.log(' Start Typing :');
rl.on('line', handleKeystroke)

In this example, the handleKeystroke function logs the input at most once every 2 seconds, regardless of how fast you type and press Enter.

Throttling in a Web Application

Now, let’s look at an example in a web application. We’ll throttle a function that handles input events on a search field, ensuring the search function is only called at most once every 300 milliseconds.

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Throttling Example</title>
</head>
<body>
  <input type="text" id="searchInput" placeholder="Type to search..." />
  <script src="app.js"></script>
</body>
</html>

TypeScript (app.ts):

First, install TypeScript and configure it if you haven’t already

npm install typescript --save-dev
npx tsc --init

Now, here’s the TypeScript code for the web application

const throttle = <T extends (...args: any[]) => void>(func: T, limit: number) => {
  let lastFunc: ReturnType<typeof setTimeout>;
  let lastRan: number | undefined;
  return function (this: unknown, ...args: Parameters<T>) {
    const context = this;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      if (lastFunc) clearTimeout(lastFunc);
      lastFunc = setTimeout(() => {
        if ((Date.now() - lastRan!) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan!));
    }
  };
};

const handleInput = throttle((event: Event) => {
  const target = event.target as HTMLInputElement;
  console.log('Search query:', target.value);
}, 300);

const searchInput = document.getElementById('searchInput') as HTMLInputElement;
searchInput.addEventListener('input', handleInput);

Explanation

  • Type Declaration
    • Throttle‘: A type for the throttle function which ensures the input function func is only called once every limit milliseconds.
  • Throttle Function
    • throttle‘ takes a function func and a limit limit in milliseconds.
    • Uses ‘lastFunc‘ to store the timeout ID and ‘lastRan‘ to store the timestamp of the last execution.
    • Checks if ‘lastRan‘ is undefined (first run) and executes ‘func‘.
    • If not the first run, clears any previous timeout and sets a new timeout to execute func after the remaining time until ‘limit‘ has passed
  • HTML and TypeScript Integration
    • The input field in the HTML triggers the throttled ‘handleInput‘ function
    • The ‘handleInput‘ function logs the search query at most once every 300 milliseconds

Conclusion

Throttling is a powerful technique to control the execution frequency of functions in JavaScript, ensuring they are called at most once in a specified period. By applying throttling in both console and web applications, you can significantly improve performance and user experience. Whether you’re managing user input, scroll events, or resize events, throttling ensures that your functions are executed efficiently and only when necessary.

For complete source – https://github.com/IndikaMaligaspe/advance-javascrips/tree/master/topics/performance-optimization

Advance Javascript: Debouncing

When writing applications in javascript, we rarely consider performance, however as the user base or application usage increases performance becomes a huge bottleneck for growth. One powerful technique to manage the frequency of function execution is debouncing. In this post, we’ll explore what debouncing is, why it’s useful, and how to implement it with examples from both a console application and a web application.

What is Debouncing?

Debouncing is a technique used to ensure that a function is only executed after a certain amount of time has passed since it was last invoked. This is particularly useful in scenarios where an event fires multiple times in quick succession, like keystrokes, window resizing, or scroll events. By debouncing, we can limit the number of times the function is called, improving performance and user experience.

Why Use Debouncing?

Imagine a search input field that sends a request to a server every time a user types a character. Without debouncing, a request would be sent for every keystroke, potentially overwhelming the server with unnecessary requests. Debouncing ensures that the request is only sent after the user has stopped typing for a specified period, reducing the number of requests and improving efficiency.

Example 1: Console application

Let’s start with a simple example in a console application using Node.js. We’ll create a debounced function that logs a message only after the user stops typing for 1 second.

First, install the ‘readline‘ module if you haven’t already:

npm install @types/node

Here’s the TypeScript code:

import * as readline from 'readline';

// Declare debounce type separately
type Debounce = <T extends (...args: any[]) => void>(func: T, wait: number) => (this: unknown, ...args: Parameters<T>) => void;

const debounce: Debounce = (func, wait) => {
  let timeout: NodeJS.Timeout | undefined;
  return function (this: unknown, ...args: Parameters<typeof func>) {
    const context = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
  };
}

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const handleInput = debounce((input: string) => {
  console.log('Executing command:', input.trim());
}, 1000);

console.log('Type a command and press Enter:');
rl.on('line', handleInput);

In the above example the the ‘handleInput’ function gets called only after 1 second after the user stops typing, allowing the application to reduce the unnecessary executions each time the user is typing.

Now, let’s look at an example in a web application. We’ll debounce a function that handles input events on a search field, ensuring the search function is only called after the user stops typing for 300 milliseconds.

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Debouncing Example</title>
</head>
<body>
  <input type="text" id="searchInput" placeholder="Type to search..." />
  <script src="app.js"></script>
</body>
</html>

JavaScript (app.js):

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

const handleInput = debounce((event) => {
  console.log('Search query:', event.target.value);
}, 300);

document.getElementById('searchInput').addEventListener('input', handleInput);

Conclusion:

Debouncing is a simple yet powerful technique to control the execution frequency of functions in JavaScript. By applying debouncing in both console and web applications, you can significantly improve performance and user experience. Whether you’re managing user input, scroll events, or resize events, debouncing ensures that your functions are executed efficiently and only when necessary. In my next article I will go into debouncing in a react application using useCallback hook and lodash ‘debounce’

For complete source – https://github.com/IndikaMaligaspe/advance-javascrips/tree/master/topics/performance-optimization

About Node.js Worker Threads and Multithreading Misconceptions

Introduction

Node.js is renowned for its single-threaded, non-blocking architecture, which is powered by the event loop and ‘libuv‘. However, this has led to a common misconception that Node.js cannot utilize multiple threads for concurrent operations. This article demystifies Node.js’s threading capabilities by introducing Worker Threads and explaining how they can be used for long-running tasks, such as database operations.

The Role of libuv in Node.js

Node.js uses ‘libuv‘, a multi-platform support library with a focus on asynchronous I/O. ‘libuv‘ manages a thread pool, which can handle file system operations, DNS resolution, and other non-JavaScript operations that require asynchronous execution. While JavaScript code runs on a single thread, ‘libuv‘ ensures that blocking operations are offloaded to these worker threads.

Node.js Worker Threads

Introduced in Node.js 10.5.0, Worker Threads provide a way to run JavaScript code in parallel threads. This is particularly useful for CPU-intensive tasks and operations that would otherwise block the event loop.

Example: Using Worker Threads for a Long-Running Database Operation

Let’s explore a practical example of using Worker Threads to handle a long-running database operation without blocking the main thread.

  • Initialize the project and install dependencies:
mkdir node-worker-example
cd node-worker-example
npm init -y
npm install express mysql2 
  • Create the project structure:
  • Worker Thread Implementation:

Create a worker.js file to define the worker:

// worker.js
const { parentPort } = require('worker_threads');
const mysql = require('mysql2/promise');

const dbConfig = {
    host: 'localhost',
    user: 'db_user',
    password: 'db_pw',
    database: 'db'
};

async function longRunningDatabaseOperation() {
    const connection = await mysql.createConnection(dbConfig);
    try {
        const [rows] = await connection.execute('SELECT SLEEP(10); SELECT * FROM test_table');
        return rows;
    } catch (error) {
        throw error;
    } finally {
        await connection.end();
    }
}

parentPort.on('message', async (message) => {
    if (message === 'start') {
        try {
            const result = await longRunningDatabaseOperation();
            parentPort.postMessage({ status: 'success', data: result });
        } catch (error) {
            parentPort.postMessage({ status: 'error', error: error.message });
        }
    }
});
  • Main Application Setup:

Create an index.js file for the main application:

// index.js
const express = require('express');
const { Worker } = require('worker_threads');

const app = express();
const port = 3000;

app.get('/non-blocking', (req, res) => {
    res.json({ status: 'non blocking finished...' });
}
app.get('/start-operation', (req, res) => {
    const worker = new Worker('./worker.js');

    worker.on('message', (message) => {
        if (message.status === 'success') {
            res.json({ status: 'completed', data: message.data });
        } else {
            res.status(500).json({ status: 'error', error: message.error });
        }
    });

    worker.on('error', (error) => {
        res.status(500).json({ status: 'error', error: error.message });
    });

    worker.on('exit', (code) => {
        if (code !== 0) {
            console.error(`Worker stopped with exit code ${code}`);
        }
    });

    worker.postMessage('start');
    res.json({ status: 'processing' });
});

app.get('/', (req, res) => {
    res.send('Hello, world!');
});

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

This example demonstrates how to leverage Worker Threads for a long-running database operation, ensuring that other requests continue to be processed efficiently. Happy coding!

Caveats

While Worker Threads in Node.js offer significant advantages for handling CPU-intensive and long-running tasks, they also come with some drawbacks.

  • Increased Complexity
    • Concurrency Issues: Introducing multiple threads can lead to issues such as race conditions, deadlocks, and other concurrency-related bugs, making the code harder to debug and maintain.
    • Communication Overhead: Communicating between the main thread and worker threads requires serialization and deserialization of messages, which can introduce overhead and complexity.
  • Performance Considerations
    • Thread Creation Overhead: Creating and managing worker threads incurs some overhead, which might negate the performance benefits for small or simple tasks.
    • Resource Consumption: Each worker thread consumes additional memory and CPU resources. For tasks that are not CPU-bound or do not benefit significantly from parallel execution, this can lead to inefficiencies.
  • Debugging and Profiling
    • Complex Debugging: Debugging issues in a multithreaded environment is generally more complex than in a single-threaded one. Tools and techniques for debugging need to account for the parallel execution context.
    • Profiling Challenges: Performance profiling in a multithreaded application can be more challenging, as it requires analyzing multiple execution contexts simultaneously.
  • Compatibility and Ecosystem
    • Module Compatibility: Not all Node.js modules are thread-safe or designed to work in a multithreaded environment. This can limit the choice of modules or require additional effort to ensure compatibility.
    • Library Support: While many libraries are compatible with Worker Threads, some may not be, or they may require additional configuration to work correctly in a multithreaded context.
  • Development Overhead
    • Learning Curve: Developers need to understand the nuances of working with threads, including thread synchronization, message passing, and potential pitfalls of concurrent execution.
    • Increased Code Complexity: Managing worker threads and ensuring proper communication and synchronization can increase the overall complexity of the application codebase.
  • Use Case Suitability
    • Not Always Necessary: For many I/O-bound tasks, Node.js’s non-blocking, asynchronous nature provides sufficient performance without the need for Worker Threads. Using Worker Threads for such tasks may not provide significant benefits and can complicate the architecture unnecessarily.

Conclusion

Node.js’s Worker Threads offer a powerful way to handle CPU-intensive and long-running tasks without blocking the main thread. This capability, coupled with libuv‘s asynchronous I/O operations, debunks the myth that Node.js cannot handle multithreading. By using Worker Threads, developers can build more efficient and responsive applications.

While Worker Threads provide powerful capabilities for handling CPU-intensive and long-running tasks in Node.js, they also introduce complexity and potential performance issues. It is essential to evaluate whether the benefits of using Worker Threads outweigh the drawbacks for your specific use case. For many applications, Node.js’s asynchronous, event-driven model may be sufficient, and Worker Threads should be used judiciously to avoid unnecessary complexity and resource consumption.