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__: nullcan 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 construction | Initial state | After repeated reads | After use as prototype |
|---|---|---|---|
{ __proto__: null } | DICT | DICT | FAST |
{ __proto__: null, a, b, c } | DICT | DICT | FAST |
{ a, b, c, __proto__: null } | DICT | DICT | FAST |
| Null-prototype literal with many properties | DICT | DICT | FAST |
Object.create(null) | DICT | DICT | FAST |
Object.create(null) with properties added | DICT | DICT | FAST |
Object.setPrototypeOf({ ... }, null) | FAST | FAST | FAST |
| Class instance with null-prototype class prototype | FAST | FAST | FAST |
| Object with a non-null custom prototype | FAST | FAST | FAST |
| Normal object literal | FAST | FAST | FAST |
| Frozen normal object | FAST | FAST | FAST |
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:
- They are created frequently, or are long-lived.
- They are accessed repeatedly.
- They sit on a performance-sensitive path.
- Their null prototype is not strictly required for the current construction method.
- 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.



