There is a lot of information around writing performant JavaScript and optimizing your code for the v8 engine. When you are reading through this information you will see a lot of phrases like inline caching, hidden classes, and memory offset; but what does it all mean? You'll come across quick one-liners about "always add properties to a particular instantiation of a class in the same order" or, better yet, "assign all of the objects properties in the constructor". You try to dive into the documentation only to see branching charts with a million tiny words in it that look more like Harry Potter's family tree. At the end of all this, you end up just trying to commit those one liners to memory without fully understanding why.
In this series, I am going to attempt to explain these concepts in a way where we don't need pages of flow charts, and with approachable examples (not just look at this code).
What to expect
We will start by going over the difference between dynamic and non-dynamic languages (mostly pertaining to how they store objects in memory). Then, in part 2, we will dive into the v8 engine and the methods it uses to efficiently handle the concepts we discuss in part 1. Also, in part 2, I will describe the common pitfalls and ways you can increase the performance of your code. However, you can't make the code better without first understanding the why.
Before we begin
Although I am going to try and explain these concepts in an approachable way, these are not easy concepts to grasp. Most developers can go their entire career without digging into the minute details of how a particular JavaScript engine accesses objects in memory.
Modern JavaScript Interpreters, like v8, are amazing tech and mostly handle all of this for you. Furthermore, with TypeScript, you have a compiler that can help keep you from making a lot of the common mistakes that can lead to a decrease in performance. However, taking the time to try and understand what is happening under the hood can go a long way.
The dynamic language
JavaScript is a dynamic programming language. This means that you can add, remove, and change (the type) property values of objects after they are initialized. Let's look at the following code:
// Define a simple constructor function for an employee
const employee = function(salary, position) {
this.salary = salary;
this.position = position;
};
// Instantiate a new employee
const newHire = new employee(50000, 'sales');
// Dynamically add the employee's desired pay day
newHire.payDay = 'Saturday';
After the employee object is created, their preferred payDay
is added dynamically. This is all perfectly valid JavaScript. It will run just fine and the newly hired employee will get paid every Saturday.
The difference between a non-dynamic programming language (meaning all of an objects properties are fixed before compilation) is that new properties cannot be added or removed at runtime. The benefit to a language being non-dynamic is that the values of these properties (or pointers) can be stored in memory with a fixed offset (an integer indicating the displacement between the beginning of an object in memory and any given property). This offset can be easily determined based on the properties type.
Skrrtttt... offset?!?! displacement?!?! You said this would be easy to follow and approachable!
You're right, this is why I decided to do this blog in two parts.
Memory storage displacement (offset)
The easiest way to explain this is with a simple data structure like an array:
const array = ['value1', 'value2', 'value3', 'value4'];
We know we can access a value in that array using its index:
array[2]; // this will get us the item at index 2 ('value3')
If the first value ('value1'
) is at memory position 0, moving two places to the right will give you ('value3'
). So 'value3'
has an offset of 2 from the start of where the array is stored in memory.
This is simple enough for an array, however not all objects are stored in memory sequentially like an array is. With more complex objects, like the employee function above, you can't be sure where the object, and its properties, will be stored. Thus making it harder to determine the offset between the objects 'shell' (to keep it simple) and its properties. You could have the 'shell' of the object (function employee() {}
) at position 0, then its property this.salary
at position 6 with other objects in between.
Back to dynamic vs. non-dynamic
In order to keep up with these offset values, non-dynamic languages (i'll use Java in this case) create a fixed object layout. This layout (or mapping) cannot be changed (as in changing the type), added too, or removed from at runtime. The offset is written in stone, making it easy (usually one instruction) to grab any property value of a given object.
Since you can add, remove, and even change a properties type in JavaScript at runtime, the interpreter has to allocate a new space in memory and then add a new mapping (offset value) back to the object for every change. It will then have to go back through and clean up the memory by a process called garbage collection. In order to keep up with these changes, JavaScript interpreters needed a data structure that can change at runtime.
Hash Tables
Instead of using a fixed object layout, like in the non-dynamic Java, most JavaScript interpreters use a dictionary like structure (based on a hash function) to store the objects property values in memory. This is often referred to as a hash table.
A hash table, simply put, is a collection of key/value pairs. A over-simplified version of what the employee
object might look like in a hash table would be keys mapped to the values of where the employee
object starts in memory, along with the offset value of each of it properties. We can attempt to replicate this using a Plain Old JavaScript Object:
const hashTable = {
objects: {
employee: {
employeeInstance1: {
memoryStart: 0,
properties: {
salary_offset: 4,
position_offset: 5
}
},
employeeInstance2: {
memoryStart: 12,
properties: {
salary_offset: 8,
position_offset: 12
}
}
}
}
}
If you wanted to add a property to the table (after the value is added in memory), you can simply update that objects 'bucket' in the table.
Adding payDay
to employee 1 would change the table to this:
const hashTable = {
objects: {
employee: {
employeeInstance1: {
memoryStart: 0,
properties: {
payDay_offset: 19, // added property and offset
salary_offset: 4,
position_offset: 5
}
},
employeeInstance2: {
memoryStart: 12,
properties: {
salary_offset: 8,
position_offset: 12
}
}
}
}
}
By doing this, the interpreter is able to add properties to the object anywhere in memory, because it has a dictionary (the hash table) keeping track of where the properties are located and what object they belong too.
The downside of this is that grabbing the properties value from a hash table is more computationally expensive (more instructions) than the fixed object layout of a non-dynamic language. Instead of having a direct one-to-one mapping of object/property to the offset of where the value is located, the interpreter must search through the hash table for the employee instance, grab the correct property, then use the offset value to finally get the property's value from memory!
- This is an extremely over-simplified explanation of how object memory storage hash tables work, but I think it helps in understanding at a very low level (and we are trying to keep this approachable). If you wanted to learn more you can start here and I have added a very nice blog at the bottom of this post. Just know it is extremely inefficient compared to what is coming next.
Firing on all cylinders
Since using hash tables to get property values is so inefficient, the JavaScript engine NodeJS uses, 'v8', takes a different approach. This approach is built around using Hidden Classes and made faster by Inline Caching.
OK! Now that the gritty stuff is out of the way. In part 2 of this series we will dive into hidden classes and inline caching. Once you better understand the concepts, it well help you understand those one-line suggestions, mentioned everywhere, that can make your JavaScript code more performant.
Further reading
Blog: How Java stores objects in memory
Top comments (0)