completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Tagged values, Smis & heap numbers

Learn how engines pack JavaScript values into one word with pointer tags, Smis, HeapNumbers, NaN-boxing, and measurement-friendly advice.

By the end, you can
  • 01
    Read a tagged wordExplain how the last bit of a word tells the engine whether the word holds a small whole number or the address of an object on the heap.
  • 02
    Tell what your code sees from how it is storedPredict when V8 stores a number as a Smi and when it needs a HeapNumber box, and say how Firefox's and Safari's engines store values differently.
  • 03
    Use the idea with careConnect whole-number speed, number fields that change in place, and boxing to real measuring habits, instead of rewriting clear code by guesswork.

One word, many values

In JavaScript, 42, 1.5, and NaN all have the same type: number. You never choose how a number is stored. The engine does. The engine is the program that runs your JavaScript. Chrome and Node use one called V8. Firefox uses SpiderMonkey. Safari uses JavaScriptCore.

The engine has to store every value somewhere in memory. Here are the words you need:

  • A bit is a single 0 or 1.
  • A word is a fixed group of bits that the computer handles in one go. On most computers today, a word is 64 bits.
  • The heap is the large area of memory where the engine keeps objects.
  • An address (also called a pointer) is a number that says where something sits in memory.

A representation is the way the engine stores one value in those bits. The same JavaScript number can be stored in more than one way. You can't see the difference from your code.

Definition

A tagged value is one word that holds two things:

  • a few tag bits, which say what kind of thing the word holds;
  • the payload, the rest of the bits, which hold the thing itself.

The tag tells the engine one of two things. Either the payload is the value itself, such as a small whole number. Or the payload is an address of an object on the heap.

This lesson comes after on-stack replacement in the engine track. How numbers behave in JavaScript (rounding, safe whole numbers, and so on) is covered in Numbers and Numbers in depth. This lesson is only about storage. You will meet these ideas in order:

  • tag bits at the end of a word;
  • Smis, small whole numbers stored right in the word;
  • HeapNumbers, numbers stored in a box on the heap;
  • NaN-boxing, the way Firefox and Safari pack values;
  • number fields that the engine updates in place;
  • why whole numbers can be faster than numbers with a decimal point.
Real-life analogyA phone contact with a note

A contact in your phone can show a phone number right on it. Or it can just say "see note". You can tell at a glance which kind it is. Then you either read the number, or go and look at the note.

In real life: A phone number is written on the contact
In JavaScript: A Smi: the number is in the word itself
In real life: The contact says "see note"
In JavaScript: A HeapNumber or object kept on the heap
In real life: You can tell at a glance which kind it is
In JavaScript: Tag bits say: value here, or address
In real life: The note tells you where to look
In JavaScript: The address leads to the object on the heap

Pointer tagging: use the last bit as a label

The engine places every heap object at an address that is a multiple of 4 or 8. This is called alignment. In binary, a multiple of 4 or 8 always ends in 0. So the last bit of a real address is always 0, and it carries no information.

Pointer tagging uses that spare last bit as a label. In V8, the simple version of the rule is:

  • last bit 0: the rest of the word is a small whole number;
  • last bit 1: the rest of the word is an address of an object on the heap.

Real engines add more details, but this one rule is the picture to keep in mind. The code below is a model of it. It doesn't look at real memory. It applies the same rule in plain JavaScript:

  • Number.isInteger(value) checks that the value is a whole number.
  • 2 ** 31 means 2 to the power of 31, which is 2147483648.
  • Object.is(value, -0) checks for negative zero, -0. JavaScript has both 0 and -0, and Object.is can tell them apart.
A model of the last-bit rulePop out in the code editor (opens in a new tab)JavaScript
function teachingTag(value) {  const isSmi =    Number.isInteger(value) &&    value >= -(2 ** 31) &&    value <= 2 ** 31 - 1 &&    !Object.is(value, -0);  return isSmi ? "low bit 0: Smi payload" : "low bit 1: pointer to a box";} console.log(teachingTag(42));console.log(teachingTag(1.5));console.log(teachingTag(-0));

Line 2 starts the test for a small whole number. Lines 3 to 5 check that the value is whole and in range. Line 6 turns away -0. Lines 10 to 12 try three values. All three have the type number in JavaScript, but only 42 fits in the word. The other two need a box on the heap.

Left: for 42, the word holds the number 42 itself, and its last bit is 0, so it is a Smi. Right: for 1.5, the word holds an address, and its last bit is 1. The address leads to a box on the heap that holds 1.5, a HeapNumber.4242wordlast bit 0a Smi1.5addresswordlast bit 11.5heapa HeapNumber
The last bit of the word says how to read the rest. For 42 it is 0: the number is right there. For 1.5 it is 1: the word is an address, and the number waits in a box on the heap.

V8 calls a number that fits in the word a Smi. It calls the box on the heap a HeapNumber. The next two sections explain each one.

In the playground below, type a number and see how each engine model would store it. It uses two tools:

  • new ArrayBuffer(8) makes 8 empty bytes (64 bits) of memory.
  • new DataView(...) lets you write a number into those bytes and read the raw bits back.
Playground: tag one number
Read the 64 bits of a numberPop out in the code editor (opens in a new tab)JavaScript
const input = "42";const value = Number(input);const view = new DataView(new ArrayBuffer(8));view.setFloat64(0, value, false); console.log("double bits", view.getBigUint64(0, false).toString(16));console.log("Node model", Number.isInteger(value) ? "Smi candidate" : "HeapNumber");
How it would be stored42
Real 64 bits (hex)0x4045000000000000
sign / exponent0 / 1028
fraction0x5000000000000
V8 in NodeSmi

Smis from -2147483648 to 2147483647. Word 0x0000000000000054, last bit 0.

V8 in ChromeSmi

Smis from -1073741824 to 1073741823. Word 0x00000054, last bit 0.

Safari (NaN-boxing)int32

0xfffe00000000002a: a whole number, marked with Safari's 0xFFFE tag.

Firefox (NaN-boxing)int32

0xfff900000000002a: a whole number, hidden in a spare NaN pattern.

Try it yourself

42 is a Smi in V8 in Node (32-bit Smis) and a Smi in V8 in Chrome (31-bit Smis). Its real 64 bits, in hex, are 0x4045000000000000.

The 64 bits are real; your browser works them out. The engine cards are models built from the engines' published notes. They don't look inside your browser's engine.

The bits on the first card are real. JavaScript stores every number in a 64-bit format called Float64 (also called a double). Its official name is IEEE-754. The playground shows those bits in hexadecimal, or hex: base 16, where each digit stands for 4 bits. The other cards are models built from the engines' published notes. They are good for thinking, but they don't read your browser's memory.

Small whole numbers: Smis

Smi is short for "small integer". An integer is a whole number. V8 keeps a Smi right in the tagged word, so it doesn't need to make a separate box on the heap. Counters, array positions, and lengths are usually Smis. That is why engine teams care so much about them.

How small is "small"? We can check in Node. Node has a special command, %IsSmi(value), that asks V8 directly. It only works when you start Node with the flag --allow-natives-syntax, so you can't run this code in the browser:

Ask Node's V8 which numbers are SmisJavaScript
console.log("node", process.version);console.log("v8", process.versions.v8);console.log("pointer compression", process.config.variables.v8_enable_pointer_compression);console.log("sandbox", process.config.variables.v8_enable_sandbox);console.log("max smi", %IsSmi(2 ** 31 - 1));console.log("overflow", %IsSmi(2 ** 31));console.log("min smi", %IsSmi(-(2 ** 31)));console.log("underflow", %IsSmi(-(2 ** 31) - 1));console.log("minus zero", %IsSmi(-0));console.log("double", %IsSmi(1.5));
What the tests check in real Node

This lesson's tests run that code in a real Node process. They check that:

  • Node is version v22.x with V8 12.4.x;
  • pointer compression and the V8 sandbox are both off (pointer compression is explained just below);
  • 2 ** 31 - 1 is a Smi, but 2 ** 31 is not;
  • -0 and 1.5 are not Smis.

Chrome is different. Chrome uses pointer compression: it stores addresses in 32 bits instead of 64, to save memory. That leaves 31 bits for a Smi. So in 64-bit Chrome, Smis go from -(2 ** 30) to 2 ** 30 - 1. That is about one billion either way. The Node build tested here has a bigger range, about two billion either way. You can read more in The heap & pointer compression.

The code below makes the same Smi decision as the Node test, using only normal JavaScript. You can run it in the browser:

Decide Smi or HeapNumber, like Node doesPop out in the code editor (opens in a new tab)JavaScript
function nodeSmiKind(value) {  const min = -(2 ** 31);  const max = 2 ** 31 - 1;  return Number.isInteger(value) &&    value >= min &&    value <= max &&    !Object.is(value, -0)    ? "Smi"    : "HeapNumber";} console.log(nodeSmiKind(2 ** 31 - 1));console.log(nodeSmiKind(2 ** 31));console.log(nodeSmiKind(-0));console.log(nodeSmiKind(1.5));

Lines 2 and 3 set the smallest and largest Smi. Line 7 turns away -0. Lines 12 to 15 try four values. Only the first one, 2 ** 31 - 1, is a Smi.

Now watch one variable grow past the largest Smi. Step through it:

Step through x = x + 1 past the largest Smi
Step 0 of 6Ready
Your turn: follow the blue line

Watch x = x + 1 reach the largest Smi in Node, stay a Smi once more, and then need a HeapNumber.

Running in
  1. script
Next: line 10
function nodeSmiKind(value) {  return Number.isInteger(value) &&    value >= -(2 ** 31) &&    value <= 2 ** 31 - 1 &&    !Object.is(value, -0)    ? "Smi"    : "HeapNumber";} console.log(x, nodeSmiKind(x));x = x + 1;console.log(x, nodeSmiKind(x));x = x + 1;console.log(x, nodeSmiKind(x));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
x = x + 1 in the Node model. 2147483646 is a Smi. Add 1 to get 2147483647, still a Smi and the largest one. Add 1 again to get 2147483648, which is too big and becomes a HeapNumber.x = x + 12147483646Smi+ 12147483647Smithe largest one+ 12147483648HeapNumbertoo big for a Smi
Each step adds 1. The second number is the largest Smi in the Node model. The third is still a normal JavaScript number, but it is too big for a Smi, so it goes in a HeapNumber box.
Smi or HeapNumber?
  • 42
  • 2 ** 30 - 1
  • 2 ** 30
  • 2 ** 31 - 1
  • 1.5
  • -0
  • NaN
Try it yourself
0 of 7 correct

Sort each value. Use the Node range (32-bit Smis) and the Chrome range (31-bit Smis) from this lesson.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

HeapNumbers and boxing

A HeapNumber is a small object on V8's heap that holds one Float64 number. V8 uses it for every number that can't be a Smi:

  • numbers with a decimal point, like 1.5;
  • Infinity, -Infinity, and NaN;
  • -0;
  • whole numbers outside the Smi range.

Boxing means putting a value in a box like this on the heap. The tagged word then holds the box's address instead of the value.

Why can't -0 be a Smi? Look at its bits. The code below writes a number into 8 bytes and prints them in hex:

See the 64 bits of a numberPop out in the code editor (opens in a new tab)JavaScript
function float64Hex(value) {  const view = new DataView(new ArrayBuffer(8));  view.setFloat64(0, value, false);  return "0x" + view.getBigUint64(0, false).toString(16).padStart(16, "0");} console.log(float64Hex(0.1));console.log(float64Hex(-0));console.log(float64Hex(Number.NaN));

Line 3 writes the number into the 8 bytes. Line 7 prints the bits of 0.1. Binary can't write 0.1 exactly, so the bits repeat (the many 9s in the hex) and are rounded at the end. Line 8 prints the bits of -0. Only the first bit, the sign bit, is 1. For +0 every bit is 0. A Smi stores a whole number with no room for that extra sign, so -0 would turn into 0. Line 9 prints the usual bits of NaN.

Node can also show what V8 made for a value. %DebugPrint(value) prints V8's own description. Like %IsSmi, it only works with --allow-natives-syntax:

Node shows a HeapNumber (Node only)JavaScript
%DebugPrint(1.5);const sample = { x: 1.5 };%DebugPrint(sample);sample.x = sample.x + 1;%DebugPrint(sample);
What the debug output shows

The test reads the printed text and checks two things. First, 1.5 prints as a HeapNumber. Second, the field x of the object is stored inside the object, and its value is a HeapNumber, both before and after the update. The test does not check addresses, because they change on every run.

The table below lists the ways of storing numbers in this lesson. Read each row from left to right: the name, what it holds, and where you meet it.

Ways engines store numbers
NameWhat does it hold?When is it used?
V8 SmiA small whole number, kept right in the word.Counters, array positions, lengths, and other small whole numbers.
V8 HeapNumberA box on the heap that holds one 64-bit number. The word holds the box's address.Numbers with a decimal point, NaN, Infinity, -0, and whole numbers that are too big for a Smi.
NaN-boxed word (Firefox, Safari)A 64-bit word. Normal numbers sit right in it; other values hide in spare NaN bit patterns.In engines that want to keep most numbers out of boxes.
MutableHeapNumber fieldA V8 box for an object field that holds numbers with a decimal point, which V8 may change in place.When the same number field is updated again and again, so V8 doesn't make a new box each time.

NaN-boxing in Firefox and Safari

V8 uses a tag bit and HeapNumbers. SpiderMonkey (Firefox) and JavaScriptCore (Safari) use a different trick, called NaN-boxing. It works like this:

  • Every value is one 64-bit word.
  • A normal Float64 number is stored right in the word, with no box.
  • The Float64 format has a huge number of bit patterns that all mean NaN. JavaScript only needs one of them.
  • The engine uses the spare NaN patterns for everything else: whole numbers, true, addresses of objects, and so on.

Your code sees exactly the same values in every engine. Only the hidden storage is different. The block below summarizes what the engines' own source code says:

What the engines' source code says, in shortText
SpiderMonkey JS::Value source comment (Firefox):  Any NaN bit pattern means either the real NaN or a value that is not a number.  In PUNBOX64, the tag sits in five bits near the top of the word. JavaScriptCore JSCJSValue.h source comment (Safari):  0xFFFE marks 32-bit whole numbers.  0x0000 marks an address or another simple value.  Numbers are stored by adding 2^49 to their bits.

In plain words, line by line:

  • Line 1 names the file in Firefox's engine.
  • Line 2: any NaN bit pattern means either the real NaN or a value that is not a number.
  • Line 3: in the 64-bit layout (called PUNBOX64), the tag sits in five bits near the top of the word.
  • Line 5 names the file in Safari's engine.
  • Line 6: a word that starts with 0xFFFE holds a 32-bit whole number.
  • Line 7: a word that starts with 0x0000 holds an address or another simple value.
  • Line 8: numbers are stored after adding a fixed amount, 2^49, to their bits.

You can read the original notes in Mozilla's Value.h and WebKit's JSCJSValue.h.

Same answers, different storage

NaN-boxing is not part of the JavaScript language. It is just one way to store values. Every engine gives the same answer for 0.1 + 0.2. Numbers in depth covers rounding, safe whole numbers, and arithmetic. This section only shows how engines can pack values.

Number fields that change in place

A normal HeapNumber never changes. Two variables might point at the same box holding 4.2. If V8 changed the box, both variables would change, and that would break JavaScript. So when a number changes, V8 makes a new box.

Making a new box every time is slow when an object field changes again and again. So V8 has a shortcut. When a field always holds numbers with a decimal point, V8 can mark it as a double field. It then keeps that field's number in a box that it is allowed to change, called a MutableHeapNumber, and updates the box in place. The V8 team explains this in its blog post about a React slowdown.

Step through the code below. It shows what your code sees:

Step through a number field that changes
Step 0 of 7Ready
Your turn: follow the blue line

Step through a number field that changes. The steps show what your code sees; the text explains V8's hidden in-place update.

Running in
  1. script
Next: line 1
 function nudgeX(object) {  object.x = object.x + 1;  return object.x;} console.log(nudgeX(point));const saved = point.x;point.x = point.x + 1;console.log(saved);console.log(point.x);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Left: the point object holds x: 2.1 after the second update. Right: a box called saved still holds 1.1, the number it read before that update.point.xx: 2.1pointchanged to 2.1saved1.1savedstill 1.1
saved read 1.1 before line 10 ran. Line 10 changed the field in the object to 2.1. The copy in saved did not change.

saved keeps the number it read. Changing point.x later does not change it. V8's shortcut is safe only because of one rule: when a number is read out of a double field, V8 copies it into a new normal box. So saved never shares the box that changes.

Where does each fact come from? The %DebugPrint code in the previous section shows the double field as a HeapNumber. The V8 blog gives the name MutableHeapNumber and explains the update in place.

Why whole numbers can be faster

A Smi lives right in the word. So working with Smis can skip some costs:

  • The engine doesn't need to make a box on the heap.
  • The garbage collector, the part of the engine that clears away boxes nobody uses, has less to clean up.
  • The engine can use fast whole-number instructions instead of decimal-point ones.

That doesn't make doubles bad. JavaScript needs them, and they are correct for many values. It just means that busy code full of small whole numbers is cheaper for the engine to run.

A whole number, a number past the limit, and a decimalPop out in the code editor (opens in a new tab)JavaScript
function addOne(value) {  return value + 1;} console.log(Number.isInteger(addOne(41)));console.log(addOne(2 ** 31 - 1));console.log(addOne(1.5));

Line 1 defines addOne, which adds 1. Line 5 gives 42, a whole number, so it prints true. Line 6 gives 2147483648. That is still a whole number, but it is past the largest Smi in Node, so it needs a HeapNumber. Line 7 gives 2.5, which has a decimal point, so a double is the natural fit. Optimizing compilers make more choices like these. They are covered in What optimizing compilers do.

Practical use: measure first, then keep busy numbers steady

Most of the time, you should not try to make numbers into Smis by hand. The useful habits are small. They matter only for hot code: code that runs very many times, such as a chart animation, a parser, or a big grid. A profiler, like the Performance panel in your browser's developer tools, shows you which code is hot.

  • Turn user input into numbers as soon as it arrives. Then busy math always gets numbers, never sometimes strings.
  • Don't let -0, NaN, or values like 0.5 slip into counters that should be whole numbers.
  • Write clear arrays and objects. Let the engine choose Smi, double, or HeapNumber.
  • Profile first. Then try a change with real data and measure again.
Real-life analogyA label on a package

At a post office, a label tells staff how to handle a package before they open it. A small one stays on the counter. A big one goes to a shelf. The engine makes the same kind of quick choice from the tag bits.

In real life: A small package stays on the counter
In JavaScript: A Smi: the whole number is in the word
In real life: A big package gets a label with a shelf number
In JavaScript: An address: go to the heap to find the value
In real life: The label changes how staff handle it, not whose it is
In JavaScript: Storage changes the engine's work, not what your code sees

Common misconceptions

  • "A Smi is a JavaScript type." No. It is one way V8 stores numbers. typeof 1 still says "number".
  • "Node and Chrome always have the same Smi range." No. The Node build tested here has pointer compression off. Chrome has it on, so its Smis are smaller.
  • "A HeapNumber is the same as new Number(...)." No. A HeapNumber is hidden inside V8. new Number(1.5) makes a normal object that your code can see.
  • "NaN-boxing changes how math works." No. It only changes storage. The JavaScript rules still decide every answer.
  • "Code with whole numbers is always faster." No. It depends on how often the code runs, what the engine has learned about it, and what you measure.

The table below lists ideas that are easy to mix up. Read each row as: the idea, what it really is, and what it is not.

Ideas that are easy to mix up
IdeaWhat is it?What is it not?
JavaScript NumberThe number type in the JavaScript rules.How the engine stores the number right now: a Smi, a HeapNumber, or a NaN-boxed word.
SmiOne way V8 stores small whole numbers.A new JavaScript type. typeof 1 is still "number".
HeapNumberA hidden V8 box on the heap that holds one 64-bit number.The object you get from new Number(1.5), which your code can see.
Pointer compressionA V8 way to save memory by storing addresses in 32 bits. In Chrome it leaves 31 bits for a Smi.Rounding in JavaScript math, or the largest safe whole number, Number.MAX_SAFE_INTEGER.

Practice exercises

6 EXERCISES
Exercise 1 · Warm-upPredict the Node result past the limit

What does the first console.log print for 2 ** 31?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function nodeSmiKind(value) {
  return Number.isInteger(value) &&
    value >= -(2 ** 31) &&
    value <= 2 ** 31 - 1 &&
    !Object.is(value, -0)
    ? "Smi"
    : "HeapNumber";
}

console.log(nodeSmiKind(2 ** 31));
console.log(nodeSmiKind(-0));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upUse the Chrome 31-bit range

    Chrome uses pointer compression. Is 2 ** 30 a Smi or a HeapNumber there?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePrint the bits of -0

      Run or read the starter code. What hex text does it print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const view = new DataView(new ArrayBuffer(8));
      view.setFloat64(0, -0, false);
      console.log("0x" + view.getBigUint64(0, false).toString(16).padStart(16, "0"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the bug in a Smi check

        This helper returns true for values that are not Smis. Name one check that is missing.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function maybeSmi(value) {
          return Number.isInteger(value);
        }
        
        console.log(maybeSmi(2 ** 40));
        console.log(maybeSmi(-0));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeRead the NaN-boxing tag

          In JavaScriptCore, which tag marks a 32-bit whole number?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a chart or grid

            Your chart updates thousands of points in every frame. What should you do first, before you rewrite any code to get more Smis?

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz: check your understanding

              8 QUESTIONS
              Value representation quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is pointer tagging?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this Node Smi model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function nodeSmiKind(value) {
                  return Number.isInteger(value) && value <= 2 ** 31 - 1 && value >= -(2 ** 31) && !Object.is(value, -0)
                    ? "Smi"
                    : "HeapNumber";
                }
                console.log(nodeSmiKind(2 ** 31 - 1));
                console.log(nodeSmiKind(2 ** 31));

                Choose an answer to see the explanation.

              3. Question 3 of 8Why is -0 not a V8 Smi?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this Float64 bit program print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const view = new DataView(new ArrayBuffer(8));
                view.setFloat64(0, -0, false);
                console.log("0x" + view.getBigUint64(0, false).toString(16).padStart(16, "0"));

                Choose an answer to see the explanation.

              5. Question 5 of 8What does NaN-boxing use?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this copy example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const point = { x: 0.1 };
                const saved = point.x;
                point.x = point.x + 1;
                console.log(saved);
                console.log(point.x);

                Choose an answer to see the explanation.

              7. Question 7 of 8Why can whole numbers be faster than numbers with a decimal point?

                Choose an answer to see the explanation.

              8. Question 8 of 8What is the safest rule to follow in real projects?

                Choose an answer to see the explanation.

              Key takeaways

              • A tagged value is one word with tag bits and a payload. The tag says whether the payload is the value or an address.
              • A V8 Smi stores a small whole number right in the word. A HeapNumber stores any other number in a box on the heap.
              • The Node 22 build tested here has 32-bit Smis, because pointer compression is off. Chrome has pointer compression on, so it has 31-bit Smis.
              • Firefox's and Safari's engines hide other values inside spare NaN bit patterns instead.
              • Use this knowledge only after a profiler shows busy number code.

              Remember the one-liner.
              JavaScript gives you values; the engine picks the cheapest way to store each one, and your code sees the same result either way.

              Up next: Booleans inside the engine. There, true and false turn out to be two ready-made objects on the heap that never change. After that come undefined, null, and strings.

              CompleteFrontend Clear concepts. Working examples.