Optimizing JavaScript Arrays: Packed vs Holey Arrays

Optimizing JavaScript Arrays: Packed vs Holey Arrays

In JavaScript, arrays can be optimized based on the types of elements they hold and how they're structured. Understanding the difference between packed arrays and holey arrays will help you write more efficient code, especially since the JavaScript engine applies different optimizations to these arrays.

Types of Arrays in JavaScript

1) 1. Packed Arrays (Continuous Arrays)

A packed array contains elements without any gaps, and all indices from 0 to the length of the array are occupied. These arrays are highly optimized for performance by JavaScript engines.

Example:

const arr = [1, 2, 3, 4, 5]; // Packed array

2) Holey Arrays (Arrays with Holes)

A holey array has gaps, meaning some indices are empty or undefined. This type of array is less optimized compared to packed arrays due to the extra checks required when accessing elements.

Example:

const arr = [1, , 3]; // Holey array

Here, there’s a gap at index 1, making it a holey array.

Types of Optimizations in JavaScript

JavaScript engines apply different optimizations to arrays based on the type of elements they hold. The three primary categories are:

  1. SMI (Small Integers): Arrays containing only 32-bit signed integers.

  2. Packed Elements: Arrays containing a mix of data types such as numbers, strings, and floats.

  3. Double Elements: Arrays containing floating-point numbers, strings, or functions.

Packed Arrays can be of Three Types:

  1. Packed SMI: Contains only small integers.

  2. Packed Elements: Contains a mix of types (numbers, strings, etc.).

  3. Packed Double: Contains floating-point numbers.

Holey Arrays can also be of Three Types:

  • Holey SMI: Contains small integers with gaps.

  • Holey Packed Elements: Contains mixed types with gaps.

  • Holey Double: Contains floating-point numbers with gaps.

Example: Transition to less optimized version

Let’s understand how an array transitions from packed to holey and what effect this has on performance:

const arr = [1, 2, 3, 4, 5]; // Packed SMI array

This array is a packed SMI array because it contains only integers and has no gaps.

Also, this type of array is the most optimized type because of its high-performance improvement, but the elements inside it are very restrictive, we can only take integers inside a “packed_SMI_elements“ type array, not even decimal values.

Transitioning to Packed Double

arr.push(6.0); // Adds a floating-point number
console.log(arr); // [1, 2, 3, 4, 5, 6]

By adding a floating-point number, the array transitions to a packed double array.

Transitioning to Packed Elements

arr.push("7"); // Adds a string
console.log(arr); // [1, 2, 3, 4, 5, 6, '7']

When we add a string, the array becomes a packed elements array, containing mixed types.

mostly default value is "packed_SMI_elements", and when it was converted to "packed_double" after pushing 6.0 inside it.

Now there's no way we can convert it back to "packed_SMI_elements", even after deleting 6.0, we cant convert it back, once downgraded we can't convert it back to "packed_SMI_elements".

Introducing Holes in an Array

When gaps are introduced, an array becomes holey, which significantly reduces its optimization.

arr[10] = 11; // Inserts value at index 10
console.log(arr); // [1, 2, 3, 4, 5, 6.0, '7', <3 empty items>, 11]

Now the array contains gaps, making it a holey array. Once an array is downgraded to a holey state, its performance is reduced, and it can’t revert to its packed form even if the gaps are later removed.

Why Holes are Costly

When accessing elements in a holey array, the JavaScript engine goes through multiple checks, which slows down performance.

Here’s what happens when accessing arr[9] in a holey array:

  1. Bounds Check: The engine checks if the requested index is within the array's length. The bound check sees that at what position the array is starting and ending, and what the length of the array is. If the user asks anything outside this length or after the ending point of the array, it quickly returns undefined.

    but when you make holes inside an array for example:

     const arr = [1, 2, 3, 4, 5, 6, '7', <3 empty item>, 11];
     console.log(arr[9]) //undefined
    

    arr[9], now this is a difficult case, now it says array out of bound check is passed as arr[9] is less than the length of array therefore bound check is passed.

  2. hasOwnProperty(arr, 9): when we asked for arr[9], it passed the bound check as it is inside the bound so we will check at that particular index, if there is a value or property, it will be returned, but here we don’t have any value so undefined is received.

  3. hasOwnProperty(arr. prototype, 9): Then we will check its prototype that if there is any value inside its prototype, and consider the next step if there is nothing inside its prototype.

  4. hasOwnProperty(Object. prototype, 9): Now as we know js has a prototypal behavior, as it continuously keeps on checking, and as we know js arrays are also objects, therefore we will check inside the Object prototype by saying that you might have introduced such property inside Object prototype.

therefore this hasOwnProperty and holes inside array are very expensive operations.

This chain of checks makes holey arrays less efficient than packed arrays, where only the bounds check and the hasOwnProperty check are needed.

Optimized Arrays: Packed vs Holey

Packed Arrays

Packed arrays are highly optimized because fewer checks are needed to access values. For instance:

const arr = [1, 2, 3, 4, 5];
console.log(arr[2]); // Accesses element in 2 steps

Step 1: First it will check if it is out of bounds or not, and we can see that it is not out of bounds.

Step 2: Then it will check for "hasOwnProperty(arr, 2)", as it is a continuous array and value is also present there it will return the value.

Holey Arrays

In contrast, accessing values in holey arrays requires multiple checks:

const arr = [1, , 3, , 5];
console.log(arr[2]); // Requires 4 steps to check gaps and prototypes

Order of Optimization in Arrays

For packed arrays, the order of optimization is:

  1. Packed SMI > Packed Double > Packed Elements

For holey arrays, the order is:

  1. Holey SMI > Holey Double > Holey Packed Elements

Example: Creating a Holey Array

Let’s consider this example where we create an array with three empty slots:

const arr = new Array(3);
console.log(arr); // Outputs: [ <3 empty items> ]

In this case, we’ve created an array with three holes, or empty slots. JavaScript treats this as a Holey SMI array, even though the array doesn’t actually contain integers yet. These empty slots make the array less optimized.

Next, let’s fill the array with strings:

arr[0] = '1';
arr[1] = '2';
arr[2] = '3';

Now the array is downgraded to Holey Packed Elements because it contains strings and has holes. Once downgraded to a holey array, it remains less optimized, and there’s no way to improve its performance afterward.

A More Efficient Approach: Starting with an Empty Array

Instead of creating an array with predefined holes, a better approach is to start with an empty array, which is highly optimized by default:

const arr = []; // SMI

This array starts out as an SMI type array, which is the most optimized form. Now, let’s add our values:

arr.push('1');
arr.push('2');

This array will still be Packed because there are no holes, and this version is more optimized than the Holey Packed array we saw in the first example. By avoiding holes and starting with an empty array, we maintain its efficiency.

Conclusion

Packed arrays in JavaScript are the most optimized form of arrays, whereas holey arrays introduce performance overhead due to additional checks. Understanding when an array transitions from packed to holey, and how JavaScript engines optimize them, will help you write more efficient and optimized code.