- read

How to Make Your JavaScript Code Blazingly Fast With This One Simple Trick! 🔥

Shimon Moyal 18

How to Make Your JavaScript Code Blazingly Fast With This One Simple Trick! 🔥

Shimon Moyal
JavaScript in Plain English
8 min readNov 5

--

The Secret Sauce: Understanding Your VM’s Inner Workings

First up, let’s talk shop about the JavaScript Virtual Machine (VM) like V8. Picture it like the brain of your operation — it’s what turns your neat code into something the computer can understand and execute.

Let’s look at a simple example:

let simpleArray = [1, 2, 3, …]

when you access an array index like simpleArray[0] , your VM is doing a little dance behind the scenes. It’s playing Tetris with memory locations to give you the need for speed (pun intended).

But if you treat your objects like a chaotic game of Jenga, adding and removing blocks, your VM gets grumpy. It’s like trying to predict the weather on Jupiter. This unpredictability can lead to your code being up to 60 times slower. Not 60% slower, but 60 times. Yikes! 🐌

The Magic Trick: Consistency Is Your Best Friend

Here’s where the magic happens. When you’re consistent with your object shapes — that’s geek speak for the structure of your objects, the VM can go into full on Dom Toretto mode. It uses something called hidden classes (spooky, right?) to make property access faster than a cheetah on a double espresso.

But as soon as you start changing the shape by adding or removing properties, you might as well be tossing banana peels under your VM’s feet. It slips up, and down goes your performance.

The VM trying to optimize your code

The Good, The Bad, and The Snappy Code

Let’s dive into some code examples to see the good, the bad, and the turbo-charged.

Slow JavaScript Example:

function addProperty(obj, propName, value) {
obj[propName] = value; // This changes the shape of the object
}
const responseObject = { user1: 1, user2: 2 };
addProperty(responseObject, 'user3', 3); // Adding a new property

What Makes it Slow?

Shape Changes: Every time the addProperty function is called, a new property is added to the object. This changes the object’s “shape”, the set of keys it contains, which in turn can overturn the JavaScript engine’s optimizations.

When a property is added or removed, the engine might have to discard the previous optimization information and start over. This “shape change” is why the operation is slow.

Fast JavaScript Example:

function createObject(a, b, c) {
// The shape of the object is fixed and predictable to the VM
return { a, b, c };
}
const dataObject = createObject(212,2344,43545);

What Makes it Fast?

Predictable Shape:

The object is created with a fixed set of properties. There are no changes after creation, making it easier for the engine to optimize.

Hidden Class Reuse:

Since the shape of the object is consistent every time createObject is called, the JavaScript engine can reuse the hidden class that it creates for this shape. This reuse allows for very fast property access because the engine knows exactly where in memory each property is.

Why Object Shape Matters:

When you access a property on an object, the engine doesn’t want to search through all the properties to find it. Instead, it wants to go directly to the property’s location in memory. If the shape of the object is known, the engine can remember where each property is located (this is called an “inline cache”). However, if the shape changes (like in the slow example above), the engine must “re-learn” the property locations, which is much slower.

A few tips:

For optimal performance, especially in critical sections of code where you are accessing properties frequently, it is better to:

  • Initialize all properties when you create an object: Even if some properties might be undefined initially.
  • Avoid adding or deleting properties: This keeps the object’s shape stable.
  • Reuse object shapes when possible: Create factory functions that always produce objects with the same set of properties.

By following these practices, you help the JavaScript engine to optimize your code, resulting in faster execution.

The Toretto Mode

Now let’s discuss common use cases

When dealing with objects from an external source, such as an API response or DOM elements, it’s beneficial for performance to normalize these objects into a consistent shape before using them. This allows the JavaScript engine to optimize access to these objects because the shape (the set of keys) is predictable and doesn’t change. This practice is especially valuable in cases where you will be reading from the objects frequently.

Let’s consider a real-life example: fetching a user profile from an API.

Slow Version:

In the slow version, properties are added to the object one by one, which can cause the JavaScript engine to deoptimize access to the object due to shape changes.

function fetchUserProfile(url) {
fetch(url)
.then(response => response.json())
.then(user => {
const userProfile = {};
if (user.name) {
userProfile.name = user.name;
}
if (user.age) {
userProfile.age = user.age;
}
if (user.email) {
userProfile.email = user.email;
}
// … more properties
return userProfile;
});
}

Fast Version:

In the fast version, we create an object with a known shape from the start, even if some properties might be undefined. This consistency allows the JavaScript engine to optimize property access.

function fetchUserProfile(url) {
return fetch(url)
.then(response => response.json())
.then(user => {
// Create an object with a consistent shape
const userProfile = {
name: user.name || undefined,
age: user.age || undefined,
email: user.email || undefined,
// … initialize all expected properties
};
return userProfile;
});
}

In the fast version, even if the user object doesn’t have all the properties we are assigning to userProfile, we still define all the keys we expect with either their corresponding values or undefined. This way, the shape of userProfile remains consistent, which is beneficial for performance when accessing its properties later on.

The VM is working hard to gobble up all your mistakes

This practice is vital in performance critical applications, where optimizations can lead to huge improvements in execution speed.

If the above example reminds you of something, it’s because this pattern looks like a factory pattern, it follows a principle similar to that of a factory function by creating an object with a predefined shape but it’s not exactly it. In JavaScript, a factory pattern typically involves a dedicated function that constructs and returns new objects. Factory functions are especially useful when the creation process is complex or when you need to do some extra setup work.

In the given fast example, we see an approach to creating an object with a consistent shape. To make it more aligned with the factory pattern, you might encapsulate the object creation in a dedicated function, like so:

function createUserProfile(name, age, email) {
// Factory function to create a user profile object
return {
name: name || undefined,
age: age || undefined,
email: email || undefined,
// … any other properties
};
}
function fetchUserProfile(url) {
return fetch(url)
.then(response => response.json())
.then(user => {
// Use the factory function to create an object with a consistent shape
return createUserProfile(user.name, user.age, user.email);
});
}

In this version, createUserProfile is a factory function that always creates an object with the same shape, which is beneficial for optimization. The fetchUserProfile function uses this factory to create a new userProfile object.

Now let’s discuss another common example, when working with the DOM, you often need to read information from HTML elements and then use this data in your application. Keeping the object’s shape consistent is important for performance, especially when you’re performing these operations repeatedly.

Here’s an example that demonstrates a slow code example where the object’s shape changes, and a fast approach where the object’s shape is predictable and consistent.

Slow code example (Changing Object Shape)

function getUserData() {
const userObject = {};
const userName = document.querySelector('#input-name');
if (nameElement) {
userObject.name = nameElement.textContent;
}
const userAge = document.querySelector('#input-age');
if (ageElement) {
userObject.age = parseInt(ageElement.textContent);
}
// Each time this function is called, it may or may not add new properties
// This can lead to a changing object shape
return userObject;
}

Fast code example (Consistent Object Shape)

function createUserData(name = undefined, age = undefined) {
// Factory function that always returns an object with the same shape
return { name, age };
}
function getUserData() {
const userName = document.querySelector('#input-name');
const userAge = parseInt(document.querySelector('#input-age')?.textContent);
// The object's shape is consistent, regardless of whether the elements exist
return createUserData(userName?.textContent, Number.isNaN(userAge) ? undefined : userAge);
}

In the fast approach, the createUserData factory function ensures that the object returned will always have the same shape, which is beneficial for the JavaScript engine’s optimization processes. The getUserData function uses this factory function to create the profile data object, and it handles missing DOM elements by providing undefined as the default value, maintaining the object’s shape.

Fast DOM

By using the optional chaining operator (?.) and the nullish coalescing operator (??), you can further refine the function to handle cases where DOM elements might not be present:

function getUserData() {
const name = document.querySelector('#input-name')?.textContent ?? undefined;
const ageText = document.querySelector('#input-age')?.textContent ?? undefined;
const age = ageText ? parseInt(ageText) : undefined;
// The object's shape is consistent
return createUserData(name, age);
}

This approach ensures that the object’s shape remains constant, even if certain elements are not found in the DOM, which is very common in dynamic web applications because sometimes an element has not been rendered or the elements are rendered out of order.

Want to learn more?

If you’re digging this and want to dive deeper into JavaScript performance and what actually goes on under the hood, check out the course that’s hotter than The Carolina Reaper: “Bare Metal JavaScript: The JavaScript Virtual Machine” by Miško Hevery.

So keep your objects in shape!

The VM to your objects

Discover a world of knowledge at LearningJournal.dev! My blog covers a wide range of topics, from technology and personal development to lifestyle and current events. I strive to provide engaging, informative, and thought-provoking content that will expand your horizons and fuel your curiosity. Join me on this journey of learning and growth as we explore new ideas and share valuable insights to help you grow both personally and professionally.

In Plain English

Thank you for being a part of our community! Before you go: