October 2, 2026
V8 null-prototype objects and Node.js performance optimization

V8 Null-Prototype Objects: Hidden Performance Cost in Node.js

JavaScript developers often use null-prototype objects when they need a clean dictionary-like data structure.

A common example looks like this:

const state = {
  __proto__: null,
  status: 'ready',
  value: 42
};

At first glance, this seems like an ideal way to create a simple object without inheriting anything from Object.prototype.

And from a JavaScript semantics perspective, that can be exactly what you want.

But there is an important V8 implementation detail that is easy to miss:

An object created with __proto__: null can start its lifetime in V8’s dictionary-property mode instead of the usual fast-property mode.

That difference matters when the object is created or accessed frequently.

In ordinary application code, the overhead may be irrelevant. But inside a runtime such as Node.js, where the same object can be accessed repeatedly on a hot execution path, the representation chosen by V8 can become measurable.

Recent performance work around Node.js Web Streams provides a useful real-world example of this behavior.

Let’s break down what happens and why changing the way an object is created can sometimes produce a surprisingly large performance improvement.


What Does a Null Prototype Actually Mean?

Every ordinary JavaScript object normally has a prototype chain.

For example:

const user = {
  name: 'Alice'
};

Conceptually, property lookup can follow a chain similar to:

user
  ↓
Object.prototype
  ↓
null

This means properties that are not found directly on user can potentially be looked up on Object.prototype.

A null-prototype object removes that inherited layer:

const user = {
  __proto__: null,
  name: 'Alice'
};

The resulting relationship is effectively:

user
  ↓
null

This is useful for dictionary-style objects because names such as toString, constructor, or hasOwnProperty do not come from Object.prototype.

Another common way to create the same kind of object is:

const data = Object.create(null);

Both approaches produce objects whose prototype is null.

From a JavaScript language perspective, this is perfectly legitimate and often useful.

The interesting part begins when we look underneath JavaScript at V8’s object representation.


How V8 Represents Object Properties

V8 does not treat every JavaScript object as a generic hash table.

For ordinary objects, V8 can use what are commonly described as maps or hidden classes to keep track of an object’s structure.

Consider:

const item = {
  id: 1,
  name: 'Book',
  price: 20
};

V8 can recognize the object’s shape:

id
name
price

Objects with compatible shapes can benefit from optimized property access.

This is one of the mechanisms that allows JavaScript engines to make frequently executed property reads much cheaper than a naive dictionary lookup.

For example:

item.price

does not necessarily mean:

search a generic hash table for "price"

Instead, V8 can use information about the object’s structure and optimized inline caches to access the property efficiently.

This is one reason JavaScript engines spend significant effort maintaining stable object shapes.


Fast Properties vs Dictionary Properties

A useful simplified distinction is between two kinds of property storage.

Fast properties

With fast properties, V8 can take advantage of the object’s known structure and optimized access paths.

Conceptually:

Object
 ├── map / hidden class
 └── property storage

Dictionary properties

Dictionary-mode objects use a more general property representation.

Conceptually:

Object
 └── property dictionary
       ├── key → value
       ├── key → value
       └── key → value

Dictionary mode is not “broken” or inherently bad.

It exists for situations where a more dynamic representation is appropriate.

The performance issue appears when an object that could otherwise benefit from fast properties ends up in dictionary mode while sitting on a frequently executed path.


The Surprising Part: __proto__: null

Now consider:

const state = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

The important observation from the V8 experiments is that the object starts with dictionary properties.

Using V8’s internal %HasFastProperties() check, the result is:

DICT

The same behavior was observed across the tested Node.js builds.

The test environment described in the investigation included a Node main build using:

V8 14.6.202.34-node.34

and:

Node v24.18.0

In both cases, objects created directly with a null prototype remained in dictionary mode during ordinary property reads.


Adding More Properties Does Not Automatically Make It Fast

One might expect the number of properties to make a difference.

For example:

const small = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

versus:

const large = {
  __proto__: null,
  p0: 0,
  p1: 1,
  p2: 2,
  p3: 3,
  p4: 4,
  p5: 5,
  p6: 6,
  p7: 7,
  p8: 8,
  p9: 9,
  p10: 0,
  p11: 1,
  p12: 2,
  p13: 3,
  p14: 4,
  p15: 5,
  p16: 6,
  p17: 7,
  p18: 8,
  p19: 9
};

The observed result is still dictionary mode.

So the important factor is not simply the number of properties.

The way the object is created matters.


Moving __proto__ Does Not Change the Result

Another interesting test is to put the prototype declaration at the end of the object literal:

const state = {
  a: 1,
  b: 2,
  c: 3,
  __proto__: null
};

It might be tempting to think that V8 could first construct the normal fast object and then process the prototype change.

The observed result does not support that assumption.

The object still starts in dictionary mode.

So these two forms behave similarly from the perspective of the tested property representation:

const a = {
  __proto__: null,
  x: 1,
  y: 2
};

and:

const b = {
  x: 1,
  y: 2,
  __proto__: null
};

Both were observed as dictionary-mode objects.


What About Object.create(null)?

Another common pattern is:

const state = Object.create(null);

You can then add properties:

state.a = 1;
state.b = 2;
state.c = 3;

The result is still dictionary-mode according to the tested V8 behavior.

So changing from:

const state = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

to:

const state = Object.create(null);

state.a = 1;
state.b = 2;
state.c = 3;

does not solve the representation issue by itself.

Both approaches begin with a null prototype, and both were observed as dictionary-mode objects.


Repeated Property Reads Do Not Fix It

This is probably the most important detail.

You might expect V8 to notice that a property is being accessed repeatedly and eventually optimize the object.

For example:

const state = {
  __proto__: null,
  value: 123
};

for (let i = 0; i < 100000; i++) {
  const value = state.value;
}

However, repeated reads do not cause the object itself to switch from dictionary mode back to fast properties in the tested cases.

The object remains:

DICT

after the repeated reads.

That means simply “warming up” the object does not solve this particular issue.

This distinction is important.

JavaScript engines perform many kinds of runtime optimization, but that does not mean every object representation automatically changes after enough property accesses.


The Interesting Exception: Using the Object as a Prototype

There is, however, a surprising exception.

When the null-prototype object is used as the prototype of another object, V8 can transition it into fast-property mode.

For example:

const proto = {
  __proto__: null,
  value: 123
};

const child = Object.create(proto);

After being used as a prototype, the tested object can become:

FAST

This is one of the key observations from the investigation.

In other words, the object can move from:

DICT

to:

FAST

when it becomes part of another object’s prototype chain.

That behavior is particularly interesting because it reveals that the null prototype itself is not the only relevant factor.

The exact way the object enters V8’s object model matters.


A Better Pattern for Some Use Cases

If you need a null prototype but also want to preserve fast properties, one useful technique is to create the ordinary object first and then change its prototype.

Instead of:

const state = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

you can use:

const state = Object.setPrototypeOf(
  {
    a: 1,
    b: 2,
    c: 3
  },
  null
);

The tested behavior is different.

The object begins as a fast-property object, and changing its prototype to null preserves the fast-property representation.

So the sequence becomes:

ordinary object
      ↓
fast properties
      ↓
prototype changed to null
      ↓
still fast

This is an important distinction.

The following two constructions may look semantically similar:

{
  __proto__: null,
  a: 1,
  b: 2
}

and:

Object.setPrototypeOf(
  { a: 1, b: 2 },
  null
);

But V8 can represent them differently.


Why This Can Matter in Node.js

For a small application object that is created once and read occasionally, this difference may be completely irrelevant.

Suppose you have:

const options = {
  __proto__: null,
  enabled: true,
  timeout: 1000
};

and you read it a few times during application startup.

There is little reason to redesign that code solely because of the V8 representation.

The situation changes when the object sits inside a hot path.

For example:

request
   ↓
stream
   ↓
internal state
   ↓
property access
   ↓
property access
   ↓
property access
   ↓
...

If the same object is accessed repeatedly while processing a large amount of data, small per-access costs can accumulate.

This is exactly why low-level runtime code needs to pay attention to object representation.


Node.js Web Streams Provide a Real Example

The performance investigation became particularly relevant in Node.js Web Streams.

Some internal state records were created using null-prototype object literals.

These objects were not simply configuration objects that were created once and forgotten.

Their fields were involved in stream processing.

That made the representation of those objects relevant to overall throughput.

The optimization work changed how those internal objects were constructed.

According to the reported benchmark results, one round of changes affecting several per-stream state records produced substantial improvements in Web Streams benchmarks.

The reported changes included approximately:

  • Pipe-to throughput: +105% to +112%
  • WritableStream creation: +204%
  • ReadableStream creation: +138%
  • TransformStream creation: +134%

These numbers are benchmark results from the specific Node.js performance work described in the investigation. They should not be interpreted as a universal percentage improvement for every application using null-prototype objects.

The important lesson is that the effect can become significant when the object participates directly in a heavily exercised runtime path.


The Second Example: Long-Lived Sentinel Objects

The same issue appeared in another part of the Web Streams implementation.

Two internal sentinel objects were involved:

kNilRequest
kNilPendingAbortRequest

These objects are created at module scope.

That might initially make their dictionary representation seem unimportant because they are not continuously recreated.

But their lifetime is long, and they are used as sentinel values in stream request state.

Their promise property is checked repeatedly during write-related operations.

So the relevant question is not simply:

“How often is this object created?”

A better question is:

“How often is this object accessed after it has been created?”

An object created once can still matter if a hot path reads it thousands or millions of times.


Changing the Sentinel Construction

The optimization used a plain object first and then removed its prototype.

Conceptually:

const sentinel = Object.setPrototypeOf(
  {
    promise: null
  },
  null
);

The exact internal implementation should always be checked against the relevant Node.js source and version, but the important construction pattern is:

Create normal object
        ↓
Get fast properties
        ↓
Set prototype to null

rather than:

Create null-prototype object directly
        ↓
Dictionary properties

The reported benchmark results for this second optimization included:

  • Pipe-to: approximately +12.3% to +14.9%
  • Pipe-through with a transform: approximately +6.8%
  • Writer-driven writes: approximately +6.5% to +17.6%

Again, these are results from the specific benchmarks associated with the Node.js change.

They should not be treated as a guarantee that replacing every null-prototype object in an application will produce similar gains.


A Useful Mental Model

It helps to think about these constructions as different paths through V8.

Approach 1: Create the null-prototype object directly

const obj = {
  __proto__: null,
  a: 1,
  b: 2
};

Observed representation:

DICT

Approach 2: Create normally, then remove the prototype

const obj = Object.setPrototypeOf(
  {
    a: 1,
    b: 2
  },
  null
);

Observed representation:

FAST

Approach 3: Use a class

Another pattern is useful when you have many instances representing the same kind of state.

For example:

class State {
  constructor() {
    this.a = 1;
    this.b = 2;
    this.c = 3;
  }
}

Object.setPrototypeOf(State.prototype, null);

const state = new State();

The tested class instance remains fast.

Its prototype chain effectively becomes:

state
   ↓
State.prototype
   ↓
null

This can provide a useful structure for repeated state records.


Why Classes Can Be Useful Here

A class can be a particularly natural solution when you need to create many objects with the same fields.

Consider:

class RequestState {
  constructor() {
    this.promise = null;
    this.status = 'idle';
    this.value = null;
  }
}

Object.setPrototypeOf(RequestState.prototype, null);

Then:

const first = new RequestState();
const second = new RequestState();
const third = new RequestState();

The instances have a consistent structure.

The important property is that the instance itself can remain fast while the prototype chain terminates at null.

This is different from creating every object directly with:

{
  __proto__: null,
  ...
}

when the tested V8 behavior places those objects in dictionary mode.


What About Plain Objects?

There is another option that is often overlooked:

Just use a normal object.

For example:

const state = {
  status: 'ready',
  value: 42,
  count: 10
};

If the properties you access are own properties, they can shadow properties with the same names that might otherwise exist on Object.prototype.

For example:

const state = {
  constructor: 'custom',
  toString: 'custom'
};

Here, those names are own properties.

So if your particular use case does not actually require a null prototype, a normal object may be sufficient.

This option also allows objects with compatible shapes to benefit from normal fast-property behavior.

However, whether removing a null prototype is appropriate depends on why the null prototype was introduced in the first place.

Performance should not be the only consideration.


Security and Semantics Still Matter

This is an important qualification.

The performance observation does not mean:

“Never use __proto__: null.”

That would be an incorrect conclusion.

Null-prototype objects have legitimate uses.

They can be useful when you specifically want to prevent inherited properties from participating in property lookup.

For example, dictionary-like structures sometimes benefit from having no Object.prototype in their prototype chain.

Changing such code purely for performance can change semantics.

Therefore, the right approach is to understand why the object was originally created with a null prototype.

If the null prototype is required for correctness or isolation, consider whether an alternative construction such as:

Object.setPrototypeOf({ ... }, null)

or a class-based design can preserve the desired semantics while retaining fast properties.


Object Creation Cost Can Also Matter

The investigation also measured object creation itself.

The reported measurements showed a large difference between creating the tested null-prototype literal and creating the equivalent ordinary literal.

The null-prototype version was measured in roughly the:

500–1500 ns

range under the stated test conditions, while the corresponding ordinary literal was around:

30 ns

These are microbenchmark measurements, not universal real-world timings.

CPU, Node.js version, V8 version, benchmark methodology, compiler state and surrounding code can all affect absolute numbers.

Still, the difference demonstrates why object construction can become relevant when performed repeatedly.

If an object is created only once, this cost may be insignificant.

If the same operation happens continuously in a high-throughput loop, it becomes more interesting.


Creation Frequency Is Only Half the Story

A useful optimization habit is to consider two separate costs.

Cost 1: Construction

How often is the object created?

For example:

for (...) {
  const state = {
    __proto__: null,
    value: 1
  };
}

If this loop executes frequently, object creation overhead can matter.

Cost 2: Access

How often is the object read after creation?

For example:

const state = {
  __proto__: null,
  value: 1
};

for (...) {
  consume(state.value);
}

Here the object is created once, but its property is accessed repeatedly.

This can still make its representation relevant.

The Node.js sentinel example demonstrates the second situation.


Why Microbenchmarks Should Be Treated Carefully

V8 performance experiments are extremely useful, but microbenchmark numbers should not automatically be transferred to application-level performance.

For example, measuring:

obj.value

one million times in isolation does not reproduce the full behavior of:

  • an HTTP server
  • a database-heavy application
  • a React application
  • a CLI tool
  • a Web Streams pipeline
  • or a production Node.js service

Real programs contain many other factors.

CPU cache behavior, garbage collection, function optimization, I/O, allocation patterns, branch behavior and surrounding code can all affect the final result.

Therefore, the strongest lesson from this investigation is not a specific nanosecond number.

It is the relationship between:

object construction
        +
object representation
        +
hot-path property access

How to Check an Object’s Property Mode

V8 exposes an internal diagnostic function:

%HasFastProperties()

This is not normal JavaScript syntax available to ordinary production code.

It requires:

node --allow-natives-syntax

For example:

'use strict';

const obj = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

console.log(
  %HasFastProperties(obj)
    ? 'FAST'
    : 'DICT'
);

This can be useful when investigating V8 behavior.

But it should be treated as a diagnostic tool rather than something application code should depend on.


A Simple Experiment

You can compare the two construction strategies yourself.

'use strict';

const direct = {
  __proto__: null,
  a: 1,
  b: 2,
  c: 3
};

const changedLater = Object.setPrototypeOf(
  {
    a: 1,
    b: 2,
    c: 3
  },
  null
);

console.log(
  'direct:',
  %HasFastProperties(direct) ? 'FAST' : 'DICT'
);

console.log(
  'changed later:',
  %HasFastProperties(changedLater) ? 'FAST' : 'DICT'
);

Run it with:

node --allow-natives-syntax test.js

The behavior described in the V8 investigation is that the first object reports dictionary properties while the second remains fast.


Testing Property Access

You can also perform repeated reads:

const obj = {
  __proto__: null,
  value: 123
};

for (let i = 0; i < 100000; i++) {
  const value = obj.value;
}

console.log(
  %HasFastProperties(obj)
    ? 'FAST'
    : 'DICT'
);

The important observation is that the repeated reads do not themselves convert the object into fast-property mode.

This helps separate two concepts that are easy to confuse:

JavaScript code gets executed repeatedly

and:

The object's internal representation changes

Those are not necessarily the same thing.


A Comparison of the Tested Patterns

The following summarizes the behavior observed in the investigation:

Object constructionInitial stateAfter repeated readsAfter use as prototype
{ __proto__: null }DICTDICTFAST
{ __proto__: null, a, b, c }DICTDICTFAST
{ a, b, c, __proto__: null }DICTDICTFAST
Null-prototype literal with many propertiesDICTDICTFAST
Object.create(null)DICTDICTFAST
Object.create(null) with properties addedDICTDICTFAST
Object.setPrototypeOf({ ... }, null)FASTFASTFAST
Class instance with null-prototype class prototypeFASTFASTFAST
Object with a non-null custom prototypeFASTFASTFAST
Normal object literalFASTFASTFAST
Frozen normal objectFASTFASTFAST

This table describes the tested V8 behavior rather than a guarantee that every future V8 release will behave identically.

JavaScript engine internals can change.


What Should Node.js Developers Take From This?

The biggest practical lesson is about where you optimize.

There is little value in rewriting every occurrence of:

{ __proto__: null }

in an application.

That could make code harder to understand and potentially change semantics without producing a meaningful application-level benefit.

Instead, look for objects that satisfy several conditions:

  1. They are created frequently, or are long-lived.
  2. They are accessed repeatedly.
  3. They sit on a performance-sensitive path.
  4. Their null prototype is not strictly required for the current construction method.
  5. Benchmarks show that changing their representation improves the workload.

That is where the investigation becomes useful.


Hot Paths Are Where Small Costs Become Large

Imagine an internal object costs only a small amount more to access.

A single access may seem irrelevant.

But suppose the same operation occurs inside a heavily used stream-processing path:

1 request
    ↓
many operations
    ↓
many property reads
    ↓
many stream writes
    ↓
many state transitions

A tiny overhead can accumulate.

This is one reason runtime projects such as Node.js pay attention to object layout and V8 optimization behavior.

The goal is not to optimize every JavaScript statement.

The goal is to identify the operations that execute often enough for low-level costs to become measurable.


The Three Patterns Worth Remembering

For the specific situation discussed here, there are three useful patterns.

1. Class-Based State Objects

When you need many instances of the same state structure, a class can be appropriate:

class State {
  constructor() {
    this.value = null;
    this.status = 'idle';
  }
}

Object.setPrototypeOf(State.prototype, null);

Then:

const state = new State();

The tested instance remains fast while the prototype chain ends at null.

This is especially relevant for repeated state records.


2. Create First, Remove the Prototype Later

For a one-off object:

const state = Object.setPrototypeOf(
  {
    value: null,
    status: 'idle'
  },
  null
);

This preserves the fast-property representation observed in the tests.

It can therefore be an alternative to:

const state = {
  __proto__: null,
  value: null,
  status: 'idle'
};

when the semantics and performance requirements make the change appropriate.


3. Use a Normal Object When You Do Not Need a Null Prototype

Sometimes the simplest solution is:

const state = {
  value: null,
  status: 'idle'
};

If your code only reads own properties and does not require a prototype-free dictionary, there may be no reason to introduce a null prototype.

Normal object literals are also able to participate in V8’s fast-property mechanisms.

Again, this is a design decision rather than a universal rule.


What You Should Not Do

There are a few conclusions that should be avoided.

Do not assume every null-prototype object is a performance problem

A small object used once during startup is unlikely to deserve optimization solely because it is dictionary-mode.

Do not assume dictionary mode means “bad”

V8 uses different internal representations for different reasons.

Dictionary properties are a legitimate engine representation.

Do not optimize based only on a microbenchmark

Always measure the workload that actually matters.

Do not remove null prototypes without considering semantics

The prototype chain affects JavaScript behavior.

If the original developer deliberately removed Object.prototype, there may be a reason.

Do not assume V8 internals are permanent APIs

Engine implementation details can change between releases.

A result observed in one V8 version should be rechecked when upgrading Node.js.


A Practical Workflow for Performance Investigation

If you suspect object representation is affecting performance, a reasonable investigation process is:

Step 1: Identify the hot path

Find the code that executes frequently.

For example:

processRequest();
processStream();
handleWrite();

Step 2: Look for repeatedly accessed state

Find objects such as:

state.promise
state.status
state.value

that are read repeatedly.

Step 3: Check how those objects are created

Look for patterns such as:

{
  __proto__: null,
  ...
}

or:

Object.create(null)

Step 4: Inspect the representation

For V8-specific investigation:

%HasFastProperties(obj)

can help confirm what is happening.

Step 5: Create an alternative

For example:

Object.setPrototypeOf(
  {
    ...
  },
  null
)

or a class-based representation.

Step 6: Benchmark the real workload

Do not stop at:

obj.value

Measure the actual operation where the object matters.

Step 7: Keep the change only if it helps

Performance optimization should be driven by evidence.


Why This Matters Beyond One Node.js PR

The broader lesson is not really about one Web Streams implementation.

It is about understanding that JavaScript source code and engine-level object representation are two different layers.

Two objects can behave similarly from the perspective of JavaScript semantics:

const a = {
  __proto__: null,
  value: 1
};

and:

const b = Object.setPrototypeOf(
  { value: 1 },
  null
);

Yet V8 may represent them differently.

That difference can become visible when the object is used heavily.

Modern JavaScript engines perform sophisticated optimization, but developers working on performance-sensitive code can still benefit from understanding the engine’s object model.


The Bigger V8 Lesson: Object Shape Matters

One of the recurring themes in JavaScript performance is object shape.

Consider:

const a = {
  x: 1,
  y: 2
};

const b = {
  x: 3,
  y: 4
};

These objects have the same basic structure.

That gives V8 useful information about their layout.

Compare that with highly dynamic objects whose structure changes constantly.

The engine has more work to do when object structure becomes unpredictable.

The null-prototype behavior discussed here is another example of how object construction can influence internal representation.

The important point is that syntax that looks like a small semantic change can sometimes affect how the engine stores and accesses the object.


What About Object.freeze()?

Freezing a normal object does not create the same dictionary-mode behavior in the tested examples.

For example:

const config = Object.freeze({
  a: 1,
  b: 2,
  c: 3
});

The tested object remains fast.

So Object.freeze() should not be treated as equivalent to creating:

{
  __proto__: null
}

These operations have very different purposes.

Object.freeze() controls mutability.

A null prototype controls the prototype chain.

The V8 representation observed in the experiments is a separate implementation detail.


The Difference Between Prototype Safety and Performance

It is useful to keep two questions separate:

Question 1: What behavior do I need?

Do I need:

Object.prototype

to be absent?

If yes, a null prototype may be appropriate.

Question 2: What performance characteristics do I need?

Is this object created or accessed on a hot path?

If yes, the way the null prototype is established may matter.

These questions can have different answers.

You might need a null prototype and fast properties.

In that situation, the alternative construction patterns discussed above become useful.


When You Probably Should Not Care

Suppose you have:

const lookup = {
  __proto__: null,
  one: 1,
  two: 2
};

and the object is:

  • created once,
  • used a few times,
  • outside any performance-critical loop,
  • not involved in a high-throughput subsystem.

There is little reason to rewrite it simply because V8 reports dictionary properties.

Code clarity and correct semantics are usually more important than a theoretical optimization.

The performance investigation is most useful when the object is part of something that executes at scale.


When You Should Investigate

On the other hand, consider an internal object that:

  • is used by every stream operation,
  • is accessed repeatedly,
  • remains alive for a long time,
  • contains fields checked on every write,
  • and appears in profiling or benchmarking results.

That is a much more interesting candidate.

This is where the Node.js Web Streams work provides a concrete example.

The object itself may be tiny.

The number of times the runtime touches it is what makes its representation important.


Reproducing the Broader Test

A more complete test can compare several object construction strategies.

'use strict';

const cases = {
  'null-prototype literal': () => ({
    __proto__: null
  }),

  'null-prototype literal with properties': () => ({
    __proto__: null,
    a: 1,
    b: 2,
    c: 3
  }),

  'normal object then null prototype': () =>
    Object.setPrototypeOf(
      {
        a: 1,
        b: 2,
        c: 3
      },
      null
    ),

  'Object.create(null)': () =>
    Object.create(null),

  'normal object': () => ({
    a: 1,
    b: 2,
    c: 3
  }),

  'frozen object': () =>
    Object.freeze({
      a: 1,
      b: 2,
      c: 3
    })
};

for (const [name, make] of Object.entries(cases)) {
  const object = make();

  console.log(
    name,
    %HasFastProperties(object)
      ? 'FAST'
      : 'DICT'
  );
}

Run with:

node --allow-natives-syntax test.js

The exact output can vary with future V8 implementations, so it is better to treat this as an experiment rather than a permanent specification.


Checking a Real Node.js Object

The same idea can be applied to internal Node.js state when you have a build and an object that exposes the relevant state.

For example, the investigation checked a WritableStream instance and located its internal state using a symbol.

A diagnostic script can look like:

const ws = new WritableStream();

const kState = Object.getOwnPropertySymbols(ws)
  .find((symbol) => symbol.description === 'kState');

console.log(
  %HasFastProperties(ws[kState].inFlightWriteRequest)
    ? 'FAST'
    : 'DICT'
);

This requires:

node --allow-natives-syntax check.js

The value depends on the particular Node.js build and implementation being inspected.

This kind of test is useful when validating whether a runtime optimization actually changed the object representation you intended to change.


The Most Important Takeaway

The interesting lesson here is not:

“Null-prototype objects are slow.”

That statement would be far too broad.

A more accurate conclusion is:

The way a null-prototype object is created can affect its V8 property representation, and that difference can matter when the object is repeatedly created or accessed on a performance-sensitive path.

That distinction is important.

Null prototypes have legitimate semantic uses.

Dictionary-mode objects are legitimate V8 representations.

And most application code will never notice the difference.

The optimization becomes relevant when measurements show that the object is involved in a hot path.


Final Thoughts

JavaScript performance is often influenced by details that are invisible at the language level.

Two snippets can produce objects that look nearly identical when inspected from JavaScript:

{
  __proto__: null,
  value: 1
}

and:

Object.setPrototypeOf(
  { value: 1 },
  null
);

Yet V8 can treat their property storage differently.

That is exactly the kind of detail that becomes interesting in a runtime such as Node.js.

The Web Streams performance work demonstrates why these details deserve attention when working on infrastructure-level JavaScript.

At the same time, the practical advice is fairly simple:

Do not optimize every null-prototype object.

Instead, find the objects that actually matter.

Look at how frequently they are created.
Look at how often their properties are accessed.
Check whether they are part of a hot execution path.
Measure the existing implementation.
Then test an alternative construction strategy.

If the object needs a null prototype, you do not necessarily have to choose between semantics and performance. In the tested V8 behavior, creating a normal object first and then setting its prototype to null, or using a class with a null-prototype prototype, can preserve fast properties.

And sometimes the best optimization is surprisingly small:

Change how the object is created rather than changing what the object does.

That is a useful reminder for anyone working close to the V8 and Node.js performance layer.

Leave a comment

Your email address will not be published. Required fields are marked *

0
0
Your Cart
Empty CartYour cart is emptyReturn to Shop

Verify Before Download

Please solve the math question to start your download.

Loading...