Fast arrays & elements kinds
Learn how V8 classifies JavaScript arrays as packed, holey, Smi, double, generic, or dictionary elements, and what makes them slow.
- 01Read an array's labelTell a packed array from a holey one, and explain why the label only moves from small whole numbers to any numbers to any values.
- 02Spot slow array codeFind holes, far-apart indexes, reads past the end, and array-like objects before you blame normal arrays.
- 03Check engine facts with careRun Node-only tests that show an array's label and spare room, and keep them apart from what JavaScript itself promises.
Arrays keep their numbered values in a separate store
An array can hold any mix of values: numbers, strings, and objects. It can even have empty places. That makes arrays very flexible.
The engine is the program that runs your JavaScript. Chrome and Node use an engine called V8. V8 wants to read list[0] fast. So it keeps an array's numbered values in their own store, apart from named properties like list.name.
- The numbered values are called elements.
- The number in the square brackets, like the
0inlist[0], is called the index.
An elements kind is a label V8 puts on an array's elements. Your code can't see it. The label answers two questions:
- Is any index missing? Packed means no. Holey means yes.
- What values are stored? Small whole numbers, any numbers, any values, or a few far-apart indexes.
V8 uses the label to pick the fastest way to read and write the elements.
Two kinds of facts appear in this lesson. Keep them apart:
- Language rules are what your code can see, like an array's values and its
length. They are the same in every engine. - Engine facts are how V8 stores arrays inside. Your code can't see them, and they can change between versions. The lesson checks them in Node 22, which uses V8 12.4.
This lesson follows the V8 team's article Elements kinds in V8. The Hidden classes lesson covered named properties. This one covers numbered elements. For how arrays behave in normal code, see array basics, iterating arrays, and typed arrays.
A full tray of same-size eggs is easy to count and carry. An empty cup, a bigger egg, or a toy in the tray means you have to look more carefully.
- In real life: Every cup in the tray has an egg
- In JavaScript: A packed array of small whole numbers, like
[1, 2, 3] - In real life: One cup is empty
- In JavaScript: A holey array: each read must check first
- In real life: A bigger egg goes in
- In JavaScript: A decimal like
2.5: the array now holds any numbers - In real life: A toy goes in
- In JavaScript: A string or object: the array now holds any values
Packed arrays and holes
An array is packed when every index from 0 to length - 1 has a value. It is holey when at least one of those indexes has nothing at all. An index with nothing is called a hole.
A hole is not the same as undefined. After arr[0] = undefined, index 0 exists and holds the value undefined. A hole means index 0 does not exist at all.
The code below uses the in operator. 0 in arr is true when index 0 exists in arr, and false when it doesn't.
const packed = ["Asha", "Ravi", "Meera"];const holey = new Array(3);holey[1] = "Ravi"; console.log(0 in packed);console.log(0 in holey);console.log(holey.length);- Line 1 makes
packedwith three values, at indexes 0, 1, and 2. - Line 2 makes
holeywithnew Array(3). This sets the length to 3 but puts no values in. All three indexes are holes. - Line 3 puts
"Ravi"at index 1. Indexes 0 and 2 are still holes. - Line 5 prints
true: index 0 exists inpacked. - Line 6 prints
false: index 0 is a hole inholey. - Line 7 prints
3. The length counts the holes too.
packed, every index from 0 to 2 has a value. In holey, only index 1 has a value, so indexes 0 and 2 are holes.Why does V8 care? When it reads from a packed array, it knows a value is there. When it reads from a holey array, it must check first.
If the index is a hole, JavaScript keeps looking in the array's prototype. That is the object the array gets its methods from, Array.prototype. This extra search costs time on every read.
V8 also remembers that an array was holey. If you fill the holes later, the label stays holey. It does not switch back to packed.
The table lists the labels this lesson uses. Read each row as: the label, how an array gets it, and what it means for speed.
| Label | How an array gets it | What it means |
|---|---|---|
PACKED_SMI_ELEMENTS | No holes, and every value is a small whole number, like [1, 2, 3]. | The fastest label for whole numbers. |
PACKED_DOUBLE_ELEMENTS | No holes, and at least one value is a decimal, -0, NaN, or Infinity. | Still fast. V8 stores every value as a plain number. |
PACKED_ELEMENTS | No holes, and at least one value is not a number, like a string or an object. | V8 can hold any value, but it knows less about each one. |
HOLEY_SMI_ELEMENTS and the other HOLEY_ labels | At least one index is a hole. | Every read must first check for a hole, and maybe search the prototype. |
DICTIONARY_ELEMENTS | Only a few indexes exist, far apart, like one value at index one million. | V8 keeps a table of the indexes that exist. Reads are slower, but no memory is spent on holes. |
Small whole numbers, any numbers, or any values
The second part of the label says what values the array holds. V8 has three groups:
- Smi, short for "small integer": small whole numbers like
1,2, and3. V8 can store these straight in the array with no extra work. - Double: any number, including decimals like
2.5. "Double" is the name of the number format JavaScript uses for all its numbers. - Generic: any value at all, such as strings, objects, and numbers mixed together. V8's label for this group has no middle word:
PACKED_ELEMENTSorHOLEY_ELEMENTS.
Some numbers look simple but are not Smis:
-0is negative zero. JavaScript keeps it apart from0, so V8 can't store it as a Smi.NaNmeans "not a number". It is the result of maths that fails, like0 / 0.Infinityis bigger than every other number.
const counts = [1, 2, 3];counts.push(-0);counts.push(NaN);counts.push(Infinity); console.log(Object.is(counts[3], -0));console.log(Number.isNaN(counts[4]));console.log(counts[5] === Infinity);- Line 1 starts with three Smis.
- Line 2 pushes
-0.pushadds a value at the end of the array. - Lines 3 and 4 push
NaNandInfinity. All three new values need the double group. - Line 6 prints
true.Object.is(a, b)checks whether two values are exactly the same, and it can tell-0from0. - Line 7 prints
true.Number.isNaN(x)istrueonly whenxisNaN. - Line 8 prints
true:counts[5]isInfinity.
Normal JavaScript can't show the label. Node can, with special V8 test functions whose names start with %. For example, %HasSmiElements(arr) returns true when arr has a Smi label.
These functions only work when you start Node with node --allow-natives-syntax. They are not part of JavaScript, so never use them in a real app.
function label(array) { if (%HasDictionaryElements(array)) return "DICTIONARY_ELEMENTS"; const prefix = %HasHoleyElements(array) ? "HOLEY" : "PACKED"; if (%HasSmiElements(array)) return prefix + "_SMI_ELEMENTS"; if (%HasDoubleElements(array)) return prefix + "_DOUBLE_ELEMENTS"; if (%HasObjectElements(array)) return prefix + "_ELEMENTS"; return "OTHER_ELEMENTS";} function report(name, array) { console.log(name + ": " + label(array));} console.log("Node " + process.version);console.log("V8 " + process.versions.v8.split(".").slice(0, 2).join("."));const values = [1, 2, 3];report("packed smi", values);values.push(4.5);report("after double", values);values.push("x");report("after string", values);values.pop();values.pop();report("after popping back", values);const holey = new Array(3);report("new Array", holey);holey[0] = 1;holey[1] = 2;holey[2] = 3;report("filled holey", holey);const short = [1, 2, 3];short[10] = 1;report("write index 10", short);const minusZero = [1, 2, 3];minusZero.push(-0);report("minus zero", minusZero);const notANumber = [1, 2, 3];notANumber.push(NaN);report("NaN", notANumber);const infinity = [1, 2, 3];infinity.push(Infinity);report("Infinity", infinity);const sparse = [];sparse[1e6] = 1;report("sparse", sparse);- Lines 1 to 8 make a function,
label, that asks V8 for an array's label. It checks for a dictionary first, then for holes, then for each group of values. - Lines 10 to 12 make
report, which prints a name and the label. - Lines 14 and 15 print the Node and V8 versions.
- Line 17 prints
packed smi: PACKED_SMI_ELEMENTS. - Line 18 pushes
4.5. Line 19 printsafter double: PACKED_DOUBLE_ELEMENTS. - Line 20 pushes
"x". Line 21 printsafter string: PACKED_ELEMENTS. - Lines 22 and 23 take both new values off again.
popremoves the last value. Line 24 still printsafter popping back: PACKED_ELEMENTS. - Line 26 prints
HOLEY_SMI_ELEMENTSfornew Array(3). Lines 27 to 29 fill every hole, but line 30 still printsHOLEY_SMI_ELEMENTS. - Line 32 writes index 10 into a three-value array. Line 33 prints
HOLEY_SMI_ELEMENTS: indexes 3 to 9 are holes. - Lines 34 to 42 push
-0,NaN, andInfinityinto new arrays. Each printsPACKED_DOUBLE_ELEMENTS. - Line 44 writes index
1e6, which means one million, into an empty array. Line 45 printssparse: DICTIONARY_ELEMENTS. A later section explains this label.
The lesson test runs this code in Node 22, which uses V8 12.4. It checks every printed label, and they match the list above.
How big can a Smi be? That depends on how V8 was built. One build setting is called pointer compression. It saves memory, and Chrome usually turns it on. Node 22 turns it off.
With pointer compression off, the biggest Smi is 2 ** 31 - 1, which is 2,147,483,647. The ** operator means "to the power of".
console.log("Node " + process.version);console.log("V8 " + process.versions.v8.split(".").slice(0, 2).join("."));console.log("pointer compression", process.config.variables.v8_enable_pointer_compression);console.log("2**31 - 1", %IsSmi(2 ** 31 - 1));console.log("2**31", %IsSmi(2 ** 31));- Lines 1 and 2 print the Node and V8 versions.
- Line 3 prints
pointer compression 0. The0means the setting is off. - Line 4 prints
2**31 - 1 true.%IsSmi(n)says whether V8 can storenas a Smi. - Line 5 prints
2**31 false. One more is too big.
The test checks Node 22, V8 12.4, pointer compression 0, and the two answers true and false. In Chrome the biggest Smi can be smaller. So don't write code that depends on this number.
A label only moves one way
When you add a value that doesn't fit the current label, V8 moves the array to a label that holds more types of values. It only moves in this order:
- Smi, then double, then generic.
- Packed, then holey.
V8 does not move a label back. To do that, it would have to check every value in the array again after each change, and that would cost more time than it saves.
Follow one array's label as values go in and come out. This replay follows V8's rules, and the lesson test checks them in real Node.
script
fares.push(4.5);fares.push("₹");fares.pop();fares.pop();console.log(fares.join(","));console.log(fares.length);- Line 1 starts
fareswith small whole numbers. Its label isPACKED_SMI_ELEMENTS. - Line 2 pushes
4.5, a decimal. The label moves toPACKED_DOUBLE_ELEMENTS. - Line 3 pushes the string
"₹", the rupee sign. The label moves toPACKED_ELEMENTS. - Lines 4 and 5 pop the string and the decimal off again.
- Line 6 prints
1,2,3and line 7 prints3. The values look like the start again, but V8 keeps thePACKED_ELEMENTSlabel. The Node test earlier proves this on its lines 22 to 24.
fares to a kind that holds more types of values. After the two pops the values look the same as at the start, but the kind stays PACKED_ELEMENTS.%DebugPrint(value) is another V8 test function. It prints everything V8 knows about a value, and one of its lines is the label.
const arr = [1, 2, 3];%DebugPrint(arr);In Node 22, %DebugPrint(arr) for [1, 2, 3] prints the line elements kind: PACKED_SMI_ELEMENTS. The rest of the output, like memory addresses, changes from run to run, so the test only checks that line.
const model = createElementsKindModel();model.push(1); // PACKED_SMI_ELEMENTSmodel.push(2.5); // PACKED_DOUBLE_ELEMENTSmodel.push("₹"); // PACKED_ELEMENTSmodel.writePastEnd(); // HOLEY_ELEMENTSmodel.push(-0); // still HOLEY_ELEMENTSNo actions yet. Start with `Push 1`, then compare `Push 2.5`, `Push ₹`, and `Make a hole`.
Start with an empty array of small integers. Add values or make a hole, and watch the kind move down the list. A kind never moves back up.
The playground does not look inside your browser. It follows V8's rules in a small model written for this lesson. The lesson test runs the same steps in Node with the % functions and checks that the labels match.
Very spread-out arrays become dictionaries
An array is sparse when most of its indexes are holes. A few holes are fine: V8 keeps a holey list. But if you write one value at index one million, a list would need a million empty places first.
So V8 switches to a dictionary. A dictionary is a table that keeps only the indexes that exist, each with its value. V8's label for it is DICTIONARY_ELEMENTS. Reading from a dictionary is slower than reading from a list, because V8 has to look the index up in the table.
A phone book has a line for every number. Your phone's contact list only saves the numbers you use. When you use only a few far-apart numbers, the contact list is much smaller.
- In real life: A phone book prints a line for every number, in order
- In JavaScript: A normal list: one place for every index
- In real life: You only call one person, at number 1,000,000
- In JavaScript:
sparse[1_000_000] = "Jaipur" - In real life: Printing a million blank lines first is a waste
- In JavaScript: A normal list would need a million holes
- In real life: Your contact list saves just that one name and number
- In JavaScript: A dictionary: only the indexes that exist, each with its value
const sparse = [];sparse[1_000_000] = "Jaipur"; console.log(sparse.length);console.log(0 in sparse);console.log(1_000_000 in sparse);- Line 1 makes an empty array,
sparse. - Line 2 writes
"Jaipur"at index1_000_000. The underscores only make the number easier to read. It is one million. - Line 4 prints
1000001. The length is always the highest index plus one. - Line 5 prints
false: index 0 is a hole. - Line 6 prints
true: index one million exists.
"Jaipur". A dictionary keeps only the one entry that exists: the index and its value.Lines 43 to 45 of the Node test code earlier do the same thing. They make const sparse = [], then write sparse[1e6] = 1. In Node 22, V8 reports DICTIONARY_ELEMENTS for it.
If your numbers are really IDs, not places in a list, an array is the wrong tool. A Map is a built-in object that stores pairs of a key and a value. A Map or a plain object says what you mean, and it has no million-long gap.
Arrays keep spare room
An array's length is the number your code sees. Behind it, V8 keeps a store with room for the elements:
- This store is called the backing store.
- The number of places in it is its capacity.
Making a bigger store means copying every value into it. If V8 did that on every push, adding values would be slow. So when the store is full, V8 makes a bigger one with spare places. The next pushes go into the spare places, with no copying.
Follow the length your code sees and the capacity V8 sets aside, as values are pushed into an empty array.
script
orders.push(1);for (let ticket = 2; ticket <= 18; ticket += 1) { orders.push(ticket);}orders.push(19);console.log(orders.length);- Line 1 starts with an empty array: length 0 and capacity 0.
- Line 2 pushes
1. V8 makes a store with 17 places. - Lines 3 to 5 push 2 to 18. The 18th value doesn't fit in 17 places, so V8 makes a new store with 43 places.
- Line 6 pushes
19. It fits, so the capacity stays 43. - Line 7 prints
19. Your code only ever sees the length.
length counts the values. Capacity counts the room V8 has set aside, which includes spare slots. When the room runs out, V8 makes a bigger store with new spare slots.How does V8 pick 17 and 43? Its source code has a function, NewElementsCapacity, that takes the number of places needed now, adds half of it, and then adds 16 more. Here is that rule written in JavaScript:
function newElementsCapacity(requestedCapacity) { return requestedCapacity + (requestedCapacity >> 1) + 16;} console.log(newElementsCapacity(1));console.log(newElementsCapacity(18));console.log(newElementsCapacity(44));- Line 1 takes
requestedCapacity: the number of places needed now, which is the new length. - Line 2 is the whole rule.
requestedCapacity >> 1halves the number and drops any remainder, so18 >> 1is9. - Line 5 prints
17: 1 + 0 + 16. - Line 6 prints
43: 18 + 9 + 16. - Line 7 prints
82: 44 + 22 + 16.
The table shows the three moments when the store grew. Read each row from left to right. The test checked each capacity with %DebugPrint.
| When | Length | Capacity | Why |
|---|---|---|---|
| After the first push | 1 | 17 | The store was empty. V8 needs 1 place, so it makes 1 + 0 + 16 = 17. |
| After push 18 | 18 | 43 | The 18th value doesn't fit in 17 places. V8 makes 18 + 9 + 16 = 43. |
| After push 44 | 44 | 82 | The 44th value doesn't fit in 43 places. V8 makes 44 + 22 + 16 = 82. |
17, 43, and 82 are true for V8 12.4. Another version can pick other numbers. What stays true is that arrays grow with spare room. Don't write app code that depends on an exact capacity.
Array-like objects are not arrays
An array-like object has number keys and a length, but it is not a real array. Common examples:
arguments: an object inside every normal function (not arrow functions). It holds the values the function was called with.- Older browser lists, like the one
document.getElementsByTagName("p")returns. - Objects you build yourself, like
{ 0: "Asha", length: 1 }.
Array.isArray(value) tells you whether a value is a real array. Most array-likes have no array methods such as forEach. You can borrow one with .call: Array.prototype.forEach.call(list, fn) runs forEach with list as the array it works on. It works, but V8's fast paths are made for real arrays.
function logArguments() { console.log(Array.isArray(arguments)); console.log(arguments.length);} logArguments("Asha", "Ravi"); const list = { 0: "Asha", 1: "Ravi", length: 2 };Array.prototype.forEach.call(list, (name, index) => { console.log(index + ":" + name);});console.log(Array.from(list).join("|"));- Lines 1 to 4 make
logArguments, which prints two facts aboutarguments. - Line 6 calls it with
"Asha"and"Ravi". Line 2 printsfalse:argumentsis not an array. Line 3 prints2. - Line 8 makes
list, an object with the keys0and1and alengthof 2. - Lines 9 to 11 borrow
forEachwith.call. They print0:Ashaand1:Ravi. - Line 12 uses
Array.from(list), which builds a real array fromlist. Thenjoin("|")printsAsha|Ravi.
The table compares a real array with two kinds of array-like values. Read each row from left to right.
| Value | What it is | What to do |
|---|---|---|
| A real array | Array.isArray(value) is true, and it has every array method. | Use it as it is. V8's fast paths are made for it. |
arguments | It holds a function's values and has a length, but it is not an array. | Use rest parameters, (...args), to get a real array instead. |
| A browser list or your own object | It looks like { 0: item, length: 1 }, but it is not an array. | Call Array.from(list) once if you will run several array methods. |
If you will run several array methods on an array-like, turn it into an array once with Array.from. Inside a function, use rest parameters, (...args), instead of arguments: they give you a real array. If you only loop once, write whatever is clearest.
Use this in real code, carefully
Elements kinds explain why some array code is slow. They are not a reason to write strange code. Work in this order:
- Write clear code with normal arrays.
- Measure the part users wait for. A profiler is a browser tool that shows where the time goes.
- If the profiler points at array code, try one change, such as removing holes. Then measure again.
One easy habit: check values once, where they come into your program. Then the array only ever gets the values you expect.
const readings = [];for (const value of [18, 19, 20]) { if (Number.isFinite(value) && !Object.is(value, -0)) { readings.push(value); }} console.log(readings.join(","));- Line 1 starts with an empty array,
readings. - Line 2 loops over the numbers
18,19, and20. - Line 3 lets a number in only if
Number.isFinite(value)istrue, so it is notNaNorInfinity, and it is not-0. - Line 4 pushes the number. Line 5 closes the check.
- Line 8 prints
18,19,20.
Habits that keep arrays on V8's fast path:
- Build arrays with
[...]orpush.new Array(n)starts with holes, and the label stays holey. - Keep out
-0,NaN, andInfinityonly when your data really should be whole numbers. - Don't read past the end, like
items[3]in a three-value array. V8 has to search the prototype for index 3, and the answer is justundefined. - Use a
Mapor an object for far-apart IDs. Use an array for places in a list. - For large lists of numbers, consider a typed array. A typed array, like
Float64Array, holds only one type of number. - Measure before and after a change. V8's exact rules can change between versions.
const a = [1, 2, 3];const a = [1, 2, 3]; a.push(-0);const a = new Array(3); a[0] = a[1] = a[2] = 1;const a = [1, 2, 3]; a[10] = 1;const a = []; a[1_000_000] = 1;Array.prototype.forEach.call(arguments, fn);
Put each line of code in the group V8 gives it. Look for holes and far-apart indexes first. Then check whether it is a real array.
Common misconceptions
- "A hole is just
undefined." No. A hole is an index that doesn't exist, so0 in arraycan befalse. Anundefinedvalue is a value that does exist. - "Popping the odd value makes the array fast again." No. The values change, but V8 does not move the label back.
- "Any gap turns an array into a dictionary." No. A few holes give a holey list. The lesson shows a dictionary only for one value at index one million.
- "Elements kinds are JavaScript types." No. They are labels inside V8. Other engines store arrays in their own ways, with their own names.
- "Knowing the engine means rewriting all my arrays." No. Write clear code, then measure. Change array code only where a profiler shows it is slow.
The table lists four pairs of ideas that are easy to mix up. Read each row from left to right.
| Idea | What it means | What it is not |
|---|---|---|
| A hole | An index that doesn't exist, so 0 in arr is false. | It is not an index that holds the value undefined. |
| An element | A value stored at a number index in an array. | It is not a named property. V8 keeps named properties in a separate place. |
An array's length | The highest index plus one. Your code can read it. | It is not the capacity. V8 keeps spare room your code can't see. |
| An array-like object | An object with number keys and a length. | It is not a real array, so most array methods are missing. |
Practice exercises
Read the code and type the two lines it prints.
const fares = [1, 2, 3];
fares.push(4.5);
fares.push("₹");
fares.pop();
fares.pop();
console.log(fares.join(","));
console.log(fares.length);It prints 1,2,3 and then 3. V8 still keeps the more general label, but your code only sees the current values and the length.
Which group of values does an array move to when you push -0: Smi, double, or generic?
-0 needs the double group. After [1, 2, 3].push(-0), the Node test prints PACKED_DOUBLE_ELEMENTS.
What does line 2 print, before any value is put in?
const seats = new Array(3);
console.log(0 in seats);
seats[0] = "Asha";
console.log(seats[0]);Line 2 prints false, because index 0 is a hole. Line 3 then puts a value at index 0, so line 4 prints Asha.
Which built-in function turns an array-like object into a real array, so you can run several array methods on it?
const list = { 0: "Asha", 1: "Ravi", length: 2 };
console.log(Array.from(list).join("|"));const names = Array.from(list);
console.log(names.join("|"));Array.from(list) reads each index of list once and returns a real array. After that, every array method works the normal way.
The code prints 60, but on its last turn it reads items[3], past the end. What loop condition should replace i <= items.length?
const items = [10, 20, 30];
let total = 0;
for (let i = 0; i <= items.length; i += 1) {
total += items[i] ?? 0;
}
console.log(total);for (let i = 0; i < items.length; i += 1) {
total += items[i];
}Use i < items.length, not i <= items.length. The fixed loop stops at index 2, so it never reads items[3] and never needs the ?? 0.
A site stores a few orders by their ID numbers, such as 42 and 1_000_000. What should you use instead of an array?
These numbers are IDs, not places in a list, so use a Map or a plain object. Arrays are best when the indexes are places in order: 0, 1, 2, and so on.
Check your understanding
Question 1 of 8What is an elements kind in V8?
Choose an answer to see the explanation.
Question 2 of 8What does this code print after writing index 2?
Read the code, then predictconst seats = []; seats[2] = "Ravi"; console.log(seats.length); console.log(0 in seats);Choose an answer to see the explanation.
Question 3 of 8Which value moves
[1, 2, 3]from the Smi group to the double group?Choose an answer to see the explanation.
Question 4 of 8An array has the label
PACKED_ELEMENTSbecause of one string. What happens if you pop that string off?Choose an answer to see the explanation.
Question 5 of 8What does this capacity rule print when 18 places are needed?
Read the code, then predictfunction newElementsCapacity(requestedCapacity) { return requestedCapacity + (requestedCapacity >> 1) + 16; } console.log(newElementsCapacity(18));Choose an answer to see the explanation.
Question 6 of 8What does this borrowed
forEachprint?Read the code, then predictconst list = { 0: "Asha", 1: "Ravi", length: 2 }; Array.prototype.forEach.call(list, (name) => console.log(name));Choose an answer to see the explanation.
Question 7 of 8When is V8 likely to use
DICTIONARY_ELEMENTS?Choose an answer to see the explanation.
Question 8 of 8Which habit fits what this lesson teaches?
Choose an answer to see the explanation.
Key takeaways
- V8 keeps an array's numbered values, its elements, apart from named properties.
- A packed array has a value at every index. A holey array has holes, so each read must check first.
- The label moves from Smi to double to generic, and from packed to holey. It does not move back.
- One value far from the start can turn an array into a dictionary. Array-like objects are not real arrays.
- Arrays grow with spare room. The exact numbers belong to V8, so measure before you change code.
In one line: arrays with no holes and one type of value are fast. Holes, mixed values, and far-apart indexes give V8 more work.
Next, Functions & closures inside the engine looks at how V8 stores functions, and how it keeps the variables a closure still needs.