Skip to content

node-api: use fast properties in node_api_create_object_with_properties - #66470

Open
colinhacks wants to merge 1 commit into
nodejs:mainfrom
colinhacks:node-api-object-shape-cache
Open

colinhacks wants to merge 1 commit into
nodejs:mainfrom
colinhacks:node-api-object-shape-cache

Conversation

@colinhacks

Copy link
Copy Markdown
Contributor

Description

Objects created with node_api_create_object_with_properties() are in dictionary mode, because the call uses v8::Object::New(). Their properties are about 7x slower to read than an object literal's, and about 70x slower with a NULL prototype, where each object also gets a map of its own.

This change keeps a cache of v8::DictionaryTemplate per env, keyed by a hash of the property names. Core already creates objects from dictionary templates in cares_wrap.cc and other places.

  • A list of names passed once is only recorded by its hash.
  • When the same list comes back, a template is created for it. Later objects with those names are instances of the template, with fast properties and one shared map.
  • The first 256 lists that come back keep their template for the life of the env. Later lists keep using v8::Object::New().
  • Symbols, strings that are not one-byte, array-index names, duplicate names and lists of more than 127 names also keep using v8::Object::New(), because a template cannot hold them.
  • A prototype other than Object.prototype costs one SetPrototype() per object, as in NewDictionaryInstanceNullProto().

Fixes #66441

Benchmark

Measured with benchmark/compare.js, 30 runs per binary, both built from adcd028 on an Apple M1 Max (macOS 26.6) that was also running other work. The benchmark uses 20 properties and a NULL prototype. Every row below has p < 0.001 (Welch's t-test).

operation n before (ops/s) after (ops/s) after / before
create 100 906,183 788,484 0.87
create 1,000 826,700 1,128,979 1.37
create 10,000 1,234,144 950,498 0.77
create 100,000 1,338,166 699,711 0.52
create 1,000,000 529,317 400,732 0.76
read 100 1,427,576 2,501,004 1.75
read 1,000 3,420,600 11,972,203 3.50
read 10,000 5,376,966 19,301,626 3.59
read 100,000 6,765,430 57,977,854 8.57
read 1,000,000 6,834,671 73,790,321 10.80

The rows with method='old' (napi_create_object and napi_set_property) do not use the changed code. They moved by -1% to +7%.

Creation with a NULL prototype is slower because of the SetPrototype() call. With Object.prototype it is unchanged. The reproduction from #66441 measures both, with 10 properties and 100,000 objects, in ns per property (median of 3 runs; an object literal reads at 0.78 ns):

prototype read before read after create before create after
Object.prototype 5.67 0.72 38.4 39.3
NULL 53.08 0.84 41.6 62.2

Validation

  • python3 tools/test.py --mode=release js-native-api node-api benchmark/test-benchmark-napi
  • out/Release/cctest --gtest_filter='*NodeApi*:*Napi*'
  • make lint-cpp, git-clang-format, and ESLint on the changed JS files
  • The new test fails on the unpatched build at the first fast-properties check. It passed 300 runs in a row on the patched build.

AI disclosure: I used a coding agent for the investigation, patch, benchmarks, and PR text. It checked the template and Object::New() behavior against the V8 source, built Node.js, and ran the tests above. I approved the PR and will handle review comments myself.

node_api_create_object_with_properties() creates every object with
v8::Object::New(), which stores the properties in a dictionary. Reads
of those objects are about 7x slower than reads of an object literal,
and about 70x slower with a null prototype, because then each object
also gets a map of its own.

Keep a cache of v8::DictionaryTemplate per env, keyed by a hash of
the property names. A list of names passed once is only recorded by
its hash. When the same list is passed again, a template is created
for it, and from then on its objects are instances of the template,
with fast properties and one shared map, like object literals. The
first 256 lists that are passed again get a template and keep it for
the life of the env; later lists keep using v8::Object::New(). A
prototype other than Object.prototype costs a SetPrototype() call per
object.

Names that a template cannot hold keep using v8::Object::New():
symbols, strings that are not one-byte, array indices, duplicate
names, and lists of more than 127 names.

Add a read operation to the benchmark, which only measured creation.

Fixes: nodejs#66441
Assisted-by: Claude Code
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 22:15
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/node-api
  • @nodejs/performance

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. node-api Issues and PRs related to Node-API. labels Oct 2, 2026
@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.65432% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.43%. Comparing base (1e9fd95) to head (0087afc).
⚠️ Report is 583 commits behind head on main.

Files with missing lines Patch % Lines
src/js_native_api_v8.cc 87.34% 2 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #66470      +/-   ##
==========================================
+ Coverage   90.17%   90.43%   +0.26%     
==========================================
  Files         769      791      +22     
  Lines      261448   275644   +14196     
  Branches    49674    52861    +3187     
==========================================
+ Hits       235759   249292   +13533     
- Misses      16736    16757      +21     
- Partials     8953     9595     +642     
Files with missing lines Coverage Δ
src/js_native_api_v8.h 91.05% <100.00%> (+0.14%) ⬆️
src/js_native_api_v8.cc 77.02% <87.34%> (+0.38%) ⬆️

... and 319 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. node-api Issues and PRs related to Node-API.

Projects

Status: Need Triage

Development

Successfully merging this pull request may close these issues.

node-api: objects from node_api_create_object_with_properties are in dictionary mode

3 participants