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;
}
After minification:
function x(a,b){return a+b}
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
- 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>
- myModule.js (the module that will be dynamically loaded):
export function helloWorld() {
return 'Hello from dynamically loaded module!';
}
- 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;
});
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);
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);
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);
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);
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;
}
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');");
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>
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`;
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
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));
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
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
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}
The result of serialization will be:
{
"name": "Alice",
"age": 30
}
This is because:
- The
objWithFunction
object contains agreet
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
andage
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