Talal Zaman Abbasi - Software Engineer
CEO Bit Technologies - Best Digital Marketing Specialist in Pakistan
Talal Zaman Abbasi - Software Engineer
CEO Bit Technologies - Best Digital Marketing Specialist in Pakistan

Blog Post

Immutable JavaScript – How to Improve the Performance of Your JS Applications

February 5, 2024 Design
Immutable JavaScript – How to Improve the Performance of Your JS Applications

[ad_1]

Javascript has become a very popular programming language thanks to its growing use in frontend and backend development.

And as devs build JavaScript applications for different companies and organizations, the size and complexity of these applications can pose interesting performance challenges.

As developers, we seek to create applications that not only have high performance but also have an enhanced user experience. To do this, we must fully understand how immutability works in Javascript, as this is one factor that contributes a lot to achieving the enhanced performance we seek.

Table of Contents:

  1. What is immutability in JavaScript?
  2. Benefits of Immutability in Applications
  3. Techniques for Achieving Immutability
  4. How to Use ES6 Features for Immutability – Spread Syntax and Object.freeze()
  5. Performance Optimization through Immutability
  6. Real-World Examples of Companies and Projects Benefiting from Immutability
  7. Common Pitfalls of Immutable JavaScript
  8. Best Practices for Overcoming Immutability-Related Issues
  9. Conclusion

What is Immutability in JavaScript?

According to the Oxford English Learners Dictionary, immutability means “something that cannot be changed; that will never change,” whereas on the other hand, we have mutability, which is the direct opposite of immutability and means something that can change.

If you want to grasp the full meaning of immutability, we have to differentiate it from mutability. Mutability, in JavaScript, refers to the ability to modify the value of a variable or a data type. Immutability, on the other hand, means that once a value is created, it cannot be changed. JavaScript differentiates data types by their default characters.

Primitive data types such as strings, numbers, booleans, and symbols are immutable, while reference data types such as objects, arrays, and functions are mutable.

To explain further, let’s take a look at this simple example:

let person1 = 10;
person2 = person1
person2 = 8;

console.log(person2) // 8
console.log(person1) // 10
image-7

On examining this example, it would seem as if person1 was modified. But the variable person1 was reassigned to take the value of person2. But when we check the value of person1 in the console, we will notice that the value of person1 remains unchanged.

This means that the person2 variable is just a clone of the person1 variable that has been reassigned, and the actual value of the person1 variable was not modified.

On the other hand, if we were to try to do the same with a reference data type, here’s what would happen:

let student1 = {
    name: "Kevin",
    age: 20,
};

student2 = student1;

student2.name = "Paul";

console.log(student1) // {name: 'Paul', age: 20}
console.log(student2) // {name: 'Paul', age: 20}
Screen Shot 2023-12-21 at 9.21.34 AM
Mutable data example

On close inspection, you’ll notice that as we set student2 to be equal to student1 and reassign the value of student2.name, it also changes the value of student1.name. This means that the value of student1 was not just reassigned but modified. This proves that reference data types are mutable.

Benefits of Immutability in Applications

You may already be wondering: Isn’t it a good thing for your variable to be mutable rather than rigid? Though mutability comes with some perks, so does immutability. In this section, we will see the benefits of immutability in applications.

First of all, immutable data structures are more stable and predictable. They’re immune to unexpected alterations, rendering the code more deterministic and less prone to unforeseen bugs or side effects, which is very useful in large-scale applications.

You also have fewer bugs and data races. Immutability eliminates the possibility of accidentally modifying data in place, which can lead to data races and concurrency issues. By preventing direct modifications, immutability promotes thread safety and ensures that data remains consistent across multiple threads or processes.

Memory management and optimization also improve with immutability. It improves memory utilization by enabling safe data structure sharing, eliminating concerns about unintended modifications.

While the idea of creating copies might seem counterintuitive in pursuit of immutability, it is balanced by the benefits of structural sharing, efficient garbage collection, and the design of data structures.

These elements work to ensure that, despite the initial creation of copies for immutability, the long-term memory optimization prevails. The utilization of structural sharing minimizes the need for complete data duplication, while efficient garbage collection promptly removes unused data structures, contributing to optimal memory usage over time.

This approach not only reduces memory overhead but also optimizes resource utilization, playing a crucial role in scaling applications efficiently.

And finally, immutability produces more effective testing and debugging. Immutable data simplifies testing and debugging by providing a stable and predictable environment. Since objects cannot be modified, tests can focus on specific behaviors without worrying about external changes. This makes it easier to isolate and fix bugs.

Techniques for Achieving Immutability

You can achieve immutability in JavaScript in a lot of ways, from using a persistent data structure to using ES6 features such as Object.freeze(). This section seeks to explain these techniques for achieving immutability and how you can use them.

Introduction to Persistent Data Structures

Persistence in data structures refers to the ability to retain the previous version while accommodating changes. In traditional mutable data structures, alterations directly modify the existing data. But in persistent data structures, any modification creates a new version of the structure, leaving the original intact.

This characteristic is what makes persistent data structures inherently immutable. This promotes code reliability, efficient memory utilization, and allows for concurrent operations.

JavaScript itself, as a language, does not inherently offer persistent data structures in its standard library. But libraries and frameworks in JavaScript, like Immutable.js, offer implementations of persistent data structures.

Immutable.js provides various persistent data structures, including:

  • Persistent List
  • Persistent Maps & Sets
  • Persistent Trees

Persistent Lists

Persistent lists, like immutable linked lists, allow for efficient modifications by creating new versions while reusing the majority of the existing structure.
Let’s consider this example:

class ShoppingList {
  constructor(item, next = null) {
    this.item = item;
    this.next = next;
  }

  addItem(newItem) {
    // Create a copy of the list
    const copiedList = this.copyList();
    // Add the new item to the copied list
    return copiedList.addItemToCopy(newItem);
  }

  addItemToCopy(newItem) {
    return new ShoppingList(newItem, this);
  }

  removeItem(itemToRemove) {
    // Create a copy of the list
    const copiedList = this.copyList();
    let current = copiedList;
    let previous = null;

    while (current !== null) {
      if (current.item === itemToRemove) {
        if (previous === null) {
          return current.next;
        } else {
          previous.next = current.next;
          return copiedList;
        }
      }
      previous = current;
      current = current.next;
    }
    return copiedList;
  }

  copyList() {
    // Create a copy of the list
    let current = this;
    let newList = null;
    let newListTail = null;

    while (current !== null) {
      if (newList === null) {
        newList = new ShoppingList(current.item);
        newListTail = newList;
      } else {
        newListTail.next = new ShoppingList(current.item);
        newListTail = newListTail.next;
      }
      current = current.next;
    }
    return newList;
  }
}

// Creating a persistent shopping list
const originalList = new ShoppingList("Milk").addItem("Eggs").addItem("Bread");
const updatedList = originalList.addItem("Butter").addItem("Cheese");

// Removing an item from the list
const removedItem = updatedList.removeItem("Eggs");

// Logging the original, updated, and list after removing an item
console.log("Original List:");
console.log(originalList);

console.log("\nUpdated List:");
console.log(updatedList);

console.log("\nList after removing an item:");
console.log(removedItem);

In our code above, we define a class called ShoppingList which we use as a representation of a persistent shopping list. This persistent shopping list is one that can be modified and added to over time.

We have a constructor constructor(item, next = null) which initializes a new list node with the specified item and an optional reference to the next node.

The addItem(newItem) method creates a copy of the current list, adds the new item to the copy, and returns the modified copy. This ensures that the original list remains unchanged. The private addItemToCopy(newItem) method appends the new item to the end of the copied list. It creates a new list node with the new item and links it to the end of the copied list.

Then we have the removeItem(itemToRemove) method which is used to create a copy of the current list. It then searches for the specified item to remove, and returns the modified copy. It handles the case of removing an item from the beginning or middle of the list.

The copyList() method recursively traverses the current list, creating new list nodes in the copied list and linking them together. It ensures that the copied list accurately represents the original list.

This example demonstrates using the ShoppingList class to create a persistent shopping list. It starts with an initial list of “Milk”, “Eggs”, and “Bread”, adds “Butter” and “Cheese”, and then removes “Eggs”.

Here is the result of our code:

Screen Shot 2024-01-04 at 5.16.10 PM
Screen Shot 2024-01-04 at 5.27.59 PM
Persistent list Example

As you can see, despite updating the list, the original list still retains its original data and can be accessed.

Persistent Maps and Sets

Persistent maps and sets retain their previous versions when modified, offering efficient key-value pair storage or unique value collections with immutability.
Let’s consider this example:

class TreasureChest {
  constructor(contents = {}) {
    this.contents = { ...contents };
  }

  addTreasure(key, value) {
    const newContents = { ...this.contents, [key]: value };
    return new TreasureChest(newContents);
  }
}

// Creating a persistent treasure chest (map-like)
const originalChest = new TreasureChest({ gold: 100, gems: 5 });
const updatedChest = originalChest.addTreasure("diamonds", 10);

// Logging the original and updated chests
console.log("Original Chest:");
console.log(originalChest);

console.log("\nUpdated Chest:");
console.log(updatedChest);

Above, we have a real-world code example. I like to call her Treasure Chest. In the code, we define a class called TreasureChest to represent a treasure chest that holds various items. The constructor method initializes the treasure chest with an optional contents object, which represents the initial items in the chest. If no contents object is provided, an empty object is used.

The addTreasure method adds a new item to the treasure chest. It takes two arguments: the name of the item (key) and the quantity (value). It creates a new contents object by merging the current contents with a new entry for the specified item, ensuring that the new item is added without modifying the original contents. It then creates a new TreasureChest object using the updated contents and returns it.

From the code, you can see we created two instances for the TreasureChest class. First the originalChest and then the updatedChest. originalChest is initialized with { gold: 100, gems: 5 }, representing a chest with 100 gold coins and 5 gems. updatedChest is created using originalChest.addTreasure('diamonds', 10), adding 10 diamonds to the chest.

Screen Shot 2024-01-04 at 5.30.27 PM
Persistent Maps and Sets Example

From our console, we can confirm that this immutability technique works as our original Chest value is retrieved.

Persistent Trees

Persistent trees, such as binary trees or trie structures, are yet another technique for achieving immutability. They maintain previous versions during operations like insertion, deletion, or search. They create new tree instances upon modifications while reusing unchanged parts from the original tree.

This preservation of versions enables efficient manipulation and retrieval of data without mutating the original tree. Aside from immutability, a persistent tree also has some key characteristics, such as shared structure and efficient modifications.

These two features let us share unchanged parts between versions of our tree and enable efficient operations (such as insertion, deletion, or traversal) by reusing existing nodes and creating new branches only when necessary, respectively.
Here is an example:

class TreeNode {
  constructor(value, left = null, right = null) {
    this.value = value;
    this.left = left;
    this.right = right;
  }

  insert(newValue) {
    if (newValue < this.value) {
      // Insert to the left
      return new TreeNode(
        this.value,
        this.left ? this.left.insert(newValue) : new TreeNode(newValue),
        this.right
      );
    } else {
      // Insert to the right
      return new TreeNode(
        this.value,
        this.left,
        this.right ? this.right.insert(newValue) : new TreeNode(newValue)
      );
    }
  }
}

// Creating a simplified tree structure
const root = new TreeNode(5);
root.left = new TreeNode(3);
root.right = new TreeNode(8);

// Logging the original tree
console.log("Original Tree:");
console.log(root);

// Creating an updated tree by inserting a new value (in this case, 10)
const updatedRoot = root.insert(10);

// Logging the updated tree
console.log("\nUpdated Tree:");
console.log(updatedRoot);

In the code above, we focused on creating and modifying a TreeNode. We defined a TreeNode class that represents a node in a binary tree. A binary tree is a hierarchical data structure where each node can have up to two child nodes, referred to as the left child and the right child.

Our TreeNode class has two constructors: the default constructor, which creates a node with the specified value, and two child nodes, which can also be null. The left and right properties represent the left and right child nodes, respectively.

The TreeNode class also has the insert method, which is used to insert a new value into the tree. The method takes a single argument, which is the value to be inserted.

The insert method first compares the new value to the value of the current node. If the new value is less than the value of the current node, it is inserted to the left of the current node. If the new value is greater than the value of the current node, it is inserted to the right of the current node.

If the current node does not have a child node corresponding to the insertion direction, a new node is created and inserted as the child node. If the current node already has a child node in that direction, the insertion method is recursively called on that child node to handle the insertion.

The code then creates a simplified tree structure with the root node having value 5, a left child node with value 3, and a right child node with value 8. Later on, we insert a new value of 10 into the tree using the insert method of the root node.

Screen Shot 2024-01-04 at 5.32.43 PM
Persistent Tree Example

How to Use ES6 Features for Immutability – Spread Syntax and Object.freeze()

At the start of this tutorial, I explained the difference between mutable and immutable data types, and we saw some examples illustrating how they work.

Mutable data types such as arrays and objects are still super useful, and through the use of some ES6 features such as the spread operator and Object.freeze, we can utilize data types like arrays and objects while maintaining a degree of immutability.

The Spread Syntax

The spread operator (…) in JavaScript is a powerful tool that facilitates the creation of shallow copies of arrays, objects, and iterables. When it comes to immutability, the spread operator plays a crucial role in ensuring that the original data remains unchanged while allowing the creation of new, independent data structures.

Let’s consider an array in JavaScript:

const originalArray = [1, 2, 3, 4];

Using the spread operator, you can create an immutable copy of this array:

const immutableCopy = [...originalArray];

Here, immutableCopy holds a new array with the same elements as originalArray. Any modifications to immutableCopy won’t affect originalArray, ensuring the immutability of the original data. This technique has been adopted by a lot of developers in modern-day software development.

The Object.freeze() method

Object.freeze() is a method that can be used on reference data types. It prevents any form of modification or addition to an object, thereby creating an object with a read-only type.

Let’s consider this example:

// Creating an immutable object using Object.freeze()
const originalObj = { name: "Alice", age: 25 };
const immutableObj = Object.freeze(originalObj); // Freezes the object

// Attempting to modify a frozen object (in strict mode)
immutableObj.age = 30; // This change will not take effect (in strict mode)
console.log(originalObj);
console.log(immutableObj);

In the code, the originalObj is a simple object with two properties: name and age. The immutableObj is created by calling Object.freeze(originalObj). This effectively freezes the originalObj, preventing any further modifications to its properties.

We then try to code to modify the immutableObj by changing its age property to 30. However, since the object is frozen, this change will not take effect. In strict mode, attempting to modify a frozen object will result in a type error.

Screen Shot 2023-12-23 at 8.29.17 PM

Without strict mode, the immutableObj object remains immutable.

Screen Shot 2023-12-23 at 8.30.34 PM
Object.freeze for javaScript immutability.

Performance Optimization through Immutability

Immutability, unchangeability—you might wonder, does this cause more harm than good? Well, let’s see.

Let’s consider a few more steps in the process, like safeguarding your data. These steps are an investment in stability and predictability.

In mutable structures, whenever data changes, it involves copying or altering existing data, which can get messy, especially in large-scale applications.

When you’re dealing with tons of data and constantly modifying it in a mutable structure, your code has to keep track of those changes. This constant tracking and altering can add up, leading to higher computational costs.

On the other hand, with immutable data, once created, it stays put. This means fewer surprises and less overhead in managing changes.

Comparison of mutable vs. immutable data

When comparing mutable and immutable data structures in large-scale applications, it’s essential to highlight the fundamental differences that affect performance, memory usage, and overall stability.

Here’s a breakdown in a more technical manner:

Aspect Mutablility Immutability
Memory Management Tend to require more memory due to in-place modifications. Often use memory more efficiently by creating new instances.
Concurrent Access Prone to issues with concurrent access (race conditions). Safe for concurrent access as data doesn’t change.
Error-Prone Mutable states can lead to unintended bugs and errors. Immutable structures minimize unintended side effects, reducing errors.
Ease of Reasoning and Debugging Tracking changes can be complex, making debugging harder. Easier to reason about as data remains consistent. Debugging is more straightforward.
Scalability Can pose challenges in managing state changes at scale. Facilitate scalability due to predictable and consistent data.

As you can see from the table, there is a lot to benefit from adopting immutability.

Real-World Examples of Companies and Projects Benefiting from Immutability

Immutability in programming is a concept that has several advantages, and several companies and projects have benefited from it. Some of the companies are Facebook, Github, Netflix, WhatsApp, and Uber.

React, a Facebook library, uses immutability as a major principle, providing improved performance and responsiveness. Github, a collaborative tool, also makes use of React in its user interface, thereby benefiting from immutability. Netflix, WhatsApp, and Uber also benefit from immutability by making use of immutable data structures to produce consistent and reliable applications.

These are a few of the many examples of companies benefiting from immutability.

Common Pitfalls of Immutable JavaScript

Although immutability has its advantages, it can also be quite challenging if you don’t use it properly. Most of the time, many beneficial things come with a cost.

Immutability may cause a decrease in performance if used on large datasets, because new data structures are created instead of modifying existing ones.
Also totally learning the immutable approach to programming can be quite tasking, since it may involve learning how to use new libraries and techniques. But it’s definitely worth it.

Since immutability works hand in hand with functional programming, it’s also a good idea for someone implementing immutability to learn functional programming.

Data races and concurrency issues

When working with immutable data, data races and concurrency issues can still be potential issues. Some of these issues relate to shared reference, asynchronous operations, and state management.

While the data itself might be immutable, references to that data can still be shared among different parts of the code. And if multiple parts of the codebase hold references to the same immutable data and one of them attempts to modify it (even though it can’t change the original data), it can cause unintended consequences or unexpected behavior elsewhere in the code that relies on the unchanged data.

In JavaScript, asynchronous operations can introduce concurrency issues. Even though JavaScript is single-threaded, asynchronous functions (like fetching data from APIs or handling events) can create scenarios where different parts of the code operate at different times.

If these asynchronous operations involve shared state, even with immutable data, managing the changes or handling the state updates can lead to unexpected behavior or bugs.

While immutability helps in maintaining predictable state changes, managing the state across an application can still be a challenge. Libraries like Redux in React applications, for instance, rely heavily on immutable data principles to manage state changes effectively. But improper handling of state updates or mutations within such libraries can lead to unexpected behavior.

Since immutability can pose some challenges, especially when used in a large-scale applications, it’s a good idea to learn ways that we can overcome these challenges.

  • Selective usage: To avoid decreased performance in your projects, you can choose where to use immutable data. Since immutability relies on creating new datasets instead of modifying them, you should use it in parts of your projects that do not require high performance.
  • Avoid deep cloning: Deep clones of data structures can be resource-intensive, so it’s best to make use of shallow or partial updates if they can also provide the same results for you.
  • Implement Copy-on-Write strategy: The creation of new copies of data should only be used when you need data in a data structure to be modified. This can reduce copies that could be avoided, thereby increasing performance.

Conclusion

The benefits of leveraging immutability in your applications are clear. Immutability serves as a powerful tool that can help you reduce bugs, improve responsiveness, and prevent unnecessary re-renders.

It may still pose some challenges, but through practicing the right strategies, you can handle them and use them to develop very high-quality as well as cost-effective applications.

Immutability remains an essential component of the optimization and responsiveness of large-scale applications being built in the JavaScript community, an area where JavaScript is still growing.

As a developer, learning to apply the immutable method of programming will go a long way toward optimizing most of your projects and reducing the occurrence of bugs and errors. Learning this method of programming would be an investment that yields long-term benefits.

[ad_2]

Source link

Write a comment