DEV Community

Cover image for Improving JavaScript Performance: Techniques and Best Practices
Techelopment
Techelopment

Posted on • Updated on

Improving JavaScript Performance: Techniques and Best Practices

JavaScript is one of the most popular languages ​​used for web application development, due to its ability to make user interfaces dynamic and interactive. However, as modern web applications become more complex, performance can become a significant issue. A slow or unresponsive application can degrade the user experience, leading to an increase in abandonment rates. In this article, we will explore some of the most effective techniques for improving the performance of JavaScript code.

🔗 Do you like Techelopment? Check out the site for all the details!

1. Code Minification

What does it mean?

Minification is the process of removing whitespace, comments, and unnecessary characters from JavaScript source code, without affecting functionality. Minification tools, such as UglifyJS or Terser, reduce the size of JavaScript files, allowing browsers to download and load them faster.

How does it work?

During minification, variables with long names are shortened, unnecessary comments are removed, and whitespace is trimmed. This reduces the time it takes to download the file.

Example:

// Before minify
function sum(number1, number2) {
    return number1 + number2;
}
Enter fullscreen mode Exit fullscreen mode

After minification:

function x(a,b){return a+b}
Enter fullscreen mode Exit fullscreen mode

JavaScript code minification should not be done manually, it is usually handled automatically by tools that do it for you, reducing file size by removing whitespace, comments and other unnecessary elements. Below you will find some very popular tools for JavaScript code minification and how to configure them.

Most common tools for Automatic Minification:

1. Terser
2. UglifyJS
3. Google Closure Compiler
4. Webpack (with the integrated minification plugin)

2. Lazy Loading of Components

What does it mean?

Lazy loading is a technique that consists of loading resources only when they are actually needed. In a complex JavaScript application, loading all the modules or resources at the same time can slow down the page loading. Lazy loading solves this problem by loading only the essential resources initially and postponing the loading of the rest.

Example:

In this case, we dynamically load a JavaScript module only when it is needed, taking advantage of the dynamic import (import()) functionality, which allows loading JavaScript modules asynchronously.
Imagine we have a file structure like this:

index.html
main.js
myModule.js
Enter fullscreen mode Exit fullscreen mode

- index.html:

<!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 in JavaScript</h1>
    <button id="loadModule">Load Module</button>
    <div id="output"></div>

    <script src="main.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

- myModule.js (the module that will be dynamically loaded):

export function helloWorld() {
    return 'Hello from dynamically loaded module!';
}
Enter fullscreen mode Exit fullscreen mode

- main.js (the main JavaScript file that handles lazy loading):

document.getElementById('loadModule').addEventListener('click', async () => {
    // Dynamically import the form only when the button is clicked
    const module = await import('./myModule.js');

    // Execute a function from the newly loaded module
    const output = module.helloWorld();

    // Show output in DOM
    document.getElementById('output').textContent = output;
});
Enter fullscreen mode Exit fullscreen mode

The dynamic import returns a Promise, so we use await to wait for the module to fully load before calling the helloWorld() function from the imported module.

3. Debouncing and Throttling

Debouncing

Debouncing is a technique that allows you to limit the frequency with which a function is executed. It is particularly useful for handling events that can be triggered repeatedly in a short time, such as resizing the window or entering characters in a text field.

Example:

function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);  // Clear previous timeout
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

// Example of use:
const logTextDebounced = debounce(() => {
    console.log('Function performed with debouncing');
}, 2000);

window.addEventListener('resize', logTextDebounced);
Enter fullscreen mode Exit fullscreen mode

Throttling

"Throttling" is similar to debouncing, but instead of delaying the function until the event stops being called, it limits the rate at which the function is executed.

Example:

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

// Example of use:
const logScrollingThrottled = throttle(() => {
    console.log('Scrolling!');
}, 200);

window.addEventListener('scroll', logScrollingThrottled);
Enter fullscreen mode Exit fullscreen mode

4. Using requestAnimationFrame for Animations

When working with JavaScript animations, using setTimeout or setInterval to synchronize animations can cause stuttering and slowdown, because these methods are not synchronized with the display refresh rate. requestAnimationFrame is a browser API that optimizes animations by synchronizing them with the display refresh rate.

Example:

function animation() {
    /* ... animation logic ... */
    requestAnimationFrame(animation);
}
requestAnimationFrame(animation);
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Smoother animations
  • Less resource consumption because the animation is only executed when needed

5. Avoid Excessive Loops: Use Optimized Methods

Loops like for, while, and forEach can impact performance, especially when iterating over large amounts of data. JavaScript methods like map(), filter(), and reduce() can be more efficient and readable. Additionally, in many cases, these methods allow code to be more concise and maintainable.

Example:

// Using map to create a new array
const numbers = [1, 2, 3, 4];
const dobule = numbers.map(number => number * 2);
Enter fullscreen mode Exit fullscreen mode

6. Caching DOM Variables

Whenever you interact with the DOM via JavaScript, such as using document.getElementById or querySelector, the browser must navigate through the DOM tree to find the requested element. If this is repeated in a loop, it can significantly slow down performance. To optimize, it is good practice to store references to DOM elements in a variable.

Example:

// Inefficient
for (let i = 0; i < 1000; i++) {
    document.getElementById('my-element').innerHTML = i;
}

// Efficient
const element = document.getElementById('my-element');
for (let i = 0; i < 1000; i++) {
    element.innerHTML = i;
}
Enter fullscreen mode Exit fullscreen mode

7. Avoid overuse of eval() and with

Functions like eval() or with blocks can reduce performance because they prevent the JavaScript engine from optimizing the code. eval() runs code inside a string and can introduce security vulnerabilities, as well as slow execution, since the browser has to interpret and compile the code each time.

Example:

Avoid:

eval("console.log('Avoid eval');");
Enter fullscreen mode Exit fullscreen mode

8. Asynchronous JavaScript loading

To prevent loading JavaScript files from blocking page rendering, you can load scripts asynchronously or deferred by using the async or defer attributes in the <script> tag.

Difference between async and defer:

  • async: The script is executed asynchronously, so it is loaded in parallel with the parsing of the page and executed as soon as it is ready.
  • defer: The script is loaded in parallel, but is executed only after the page has been fully parsed. The order of execution is also respected.

Example:

<!-- Script loaded asynchronously -->
<script src="script.js" async></script>

<!-- Deferred script -->
<script src="script.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

9. Minimize Style Recalculation and Reflow

Operations that change the DOM structure or style can cause style recalculation and reflow (reorganizing the arrangement of elements). To improve performance, try to minimize the number of changes that alter the layout of the page.

Example:

Modify the DOM as little as possible:

// Avoid changing the style at each iteration
for (let i = 0; i < max; i++) {
    let size += i * window.innerWidth;
    element.style.width = `${size}px`;
}

// Better to update all changes together
let size = 0;
for (let i = 0; i < max; i++) {
    size += i * window.innerWidth;
}
element.style.width = `${size}px`;
Enter fullscreen mode Exit fullscreen mode

10. Caching using Memoization

Memoization is an optimization technique that consists of storing the results of an expensive (in terms of execution time) function to avoid recalculating the same result when the function is called again with the same arguments. This makes subsequent calls with the same inputs much faster since the function can simply return the stored value.

How does it work?

Each time the function is called, it checks whether the result for that particular set of arguments has already been calculated.
If so, it returns the stored value.
If not, it calculates the result, stores it, and then returns it.
Here is a practical example of memoization that can be used in a real-world context, such as a function that retrieves information from an API (for example, user data from a database or a remote server).

Suppose we have a function that retrieves user data via an API call. Each time the function is called, it makes a network request that can be expensive in terms of time. We can use memoization to store user data that has already been requested, avoiding making the same API call if we have already retrieved that information.

Example:

Without Memoization (multiple unnecessary requests):

async function fetchUserData(userId) {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    const data = await response.json();
    return data;
}

// Each call to fetchUserData will make an API request
fetchUserData(1001).then(data => console.log(data));
fetchUserData(1001).then(data => console.log(data));  // Repeated request
Enter fullscreen mode Exit fullscreen mode

In this example, every call for the same userId (like 1001) will make an API request even if the user has already been retrieved, increasing the wait time and overloading the server.

With Memoization:

With memoization, we can store the already retrieved user data and return it immediately if the function is called again with the same userId.

function memoizedFetchUserData() {
    const cache = {}; // Cache to store results

    return async function(userId) {
        if (cache[userId]) {
            console.log('return data from cache');
            return cache[userId];  // Returns the stored result
        }

        console.log('Retrieve data from API');
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        cache[userId] = data;  // Cache data
        return data;
    };
}

const fetchUserData = memoizedFetchUserData();

// The first time, the function retrieves data from the API
fetchUserData(1001).then(data => console.log(data));

// The second time, the function will return data from the cache
fetchUserData(1001).then(data => console.log(data));

// If we call with a new userId (e.g. say 1002) it will make a new API request
fetchUserData(1002).then(data => console.log(data));
Enter fullscreen mode Exit fullscreen mode

Common situations where Memoization is useful:

  • API Calls: As in this example, to reduce repetitive calls to an API
  • Complex Calculations: Mathematical or computationally expensive functions, such as processing large datasets
  • DOM Operations: If a function manipulates or calculates complex properties of the DOM, memoizing the results can reduce the overhead of continuous recalculation

11. Deep Clone with JSON.stringify() and JSON.parse()

A deep clone in JavaScript is a technique that allows you to create a complete and independent copy of an object, including all objects and arrays nested within it. This is different from a shallow copy, which only copies references for nested objects, allowing changes to the original to affect the copy.

A simple method for doing a deep clone is to use the combination of JSON.stringify() and JSON.parse(). But be careful: this approach only works if the object you want to clone is serializable to JSON, so it does not work with functions, Date, Map, Set objects, or objects with circular references.

Example of deep cloning with JSON.stringify() and JSON.parse():

const originalObject = {
    name: "Alice",
    details: {
        age: 30,
        skills: ["JavaScript", "React"]
    }
};

const deepClonedObject = JSON.parse(JSON.stringify(originalObject));

// By modifying the clone object, the original is not affected.
deepClonedObject.details.age = 35;
deepClonedObject.details.skills.push("Angular");

console.log(originalObject.details.age);   // 30
console.log(deepClonedObject.details.age); // 35
Enter fullscreen mode Exit fullscreen mode

Examples where JSON.stringify() would fail to clone properly:

Example of a circular reference object:

const person = {
    name: "Alice",
    age: 25
};

// Create a loop: person has a property that points to itself
person.self = person;

console.log(person);

JSON.stringify(person);  // Uncaught TypeError: Converting circular structure to JSON
Enter fullscreen mode Exit fullscreen mode

Example of an Object Containing Functions:

const objWithFunction = {
    name: "Alice",
    age: 30,
    greet: function() {
        return `Hello, my name is ${this.name}`;
    }
};

// Attempting serialization with JSON.stringify()
const serialized = JSON.stringify(objWithFunction);

console.log(serialized);  // {"name":"Alice","age":30}
Enter fullscreen mode Exit fullscreen mode

The result of serialization will be:

{
    "name": "Alice",
    "age": 30
}
Enter fullscreen mode Exit fullscreen mode

This is because:

  • The objWithFunction object contains a greet property that is a function.
  • When using JSON.stringify(), the function is not included in the resulting JSON string.
  • The resulting string includes only the properties that are serializable data types (in this case, the name and age properties).

Keep an eye on performance…always!

Optimizing the performance of a JavaScript application is essential to ensure a smooth and satisfying user experience. Techniques such as minification, lazy loading, DOM caching, and the appropriate use of methods such as requestAnimationFrame can significantly improve the speed of code execution. By implementing these best practices, you will not only make your applications faster, but also contribute to a better user experience and greater resource efficiency.


Follow me #techelopment

Official site: www.techelopment.it
Medium: @techelopment
Dev.to: Techelopment
facebook: Techelopment
instagram: @techelopment
X: techelopment
telegram: @techelopment_channel
youtube: @techelopment
whatsapp: Techelopment

Top comments (0)