Skip to content

node-api: objects from node_api_create_object_with_properties are in dictionary mode #66441

Description

@colinhacks

What is the problem this feature will solve?

The new node_api_create_object_with_properties (#59953) always returns objects in dictionary mode. It calls v8::Object::New(isolate, prototype, names, values, count), and V8 builds a dictionary there whatever the shape. So the objects are slow to read: about 8x slower than an object literal with Object.prototype, and about 75x with a NULL prototype, because then every object also gets its own map.

napi_create_object plus napi_define_properties gives fast objects, but it costs about twice as much to build. So right now an addon can have cheap object creation or fast property access, not both.

I measured it with a small addon: 10 properties, 100,000 objects, Node 26.10.0 on an M1 Max, median of 4 runs. The machine was busy, so the ratios matter more than the absolute numbers. Reads are compared with the object literal, builds with node_api_create_object_with_properties and Object.prototype.

Fast properties Read, ns per property Build, ns per property
object literal yes 0.69 4.0
napi_set_property per property yes, up to 19 properties 0.82 (1.2x) 117.5 (3.4x)
napi_define_properties yes 0.79 (1.1x) 70.3 (2.0x)
node_api_create_object_with_properties, Object.prototype no 5.75 (8.3x) 34.9
node_api_create_object_with_properties, NULL no, one map per object 52.44 (75x) 45.7 (1.3x)

At 32 properties it looks the same, except that the napi_set_property objects are in dictionary mode too.

Repro and raw output
#include <node_api.h>
#include <stdlib.h>

enum { kWithProperties, kSetProperty, kDefineProperties };

// make(names, values, prototype, count[, how]): `count` objects, each from one
// node_api_create_object_with_properties call (an undefined prototype passes NULL), or with
// `how` 1 from napi_create_object and one napi_set_property call per property, or with `how` 2
// from napi_create_object and one napi_define_properties call (the prototype is ignored for both).
static napi_value Make(napi_env env, napi_callback_info info) {
  size_t argc = 5;
  napi_value argv[5], result, proto;
  uint32_t width, count;
  int32_t how = kWithProperties;
  napi_valuetype type;
  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
  napi_get_array_length(env, argv[0], &width);
  napi_get_value_uint32(env, argv[3], &count);
  napi_typeof(env, argv[2], &type);
  proto = type == napi_undefined ? NULL : argv[2];
  if (argc > 4) napi_get_value_int32(env, argv[4], &how);

  napi_value* names = malloc(sizeof(napi_value) * width * 2);
  napi_value* values = names + width;
  napi_property_descriptor* descriptors = calloc(width, sizeof(napi_property_descriptor));
  for (uint32_t i = 0; i < width; i++) {
    napi_get_element(env, argv[0], i, &names[i]);
    napi_get_element(env, argv[1], i, &values[i]);
    descriptors[i].name = names[i];
    descriptors[i].value = values[i];
    descriptors[i].attributes = napi_default_jsproperty;
  }

  napi_create_array_with_length(env, count, &result);
  for (uint32_t i = 0; i < count; i++) {
    napi_handle_scope scope;
    napi_value obj;
    napi_open_handle_scope(env, &scope);
    if (how == kSetProperty) {
      napi_create_object(env, &obj);
      for (uint32_t j = 0; j < width; j++) napi_set_property(env, obj, names[j], values[j]);
    } else if (how == kDefineProperties) {
      napi_create_object(env, &obj);
      napi_define_properties(env, obj, width, descriptors);
    } else {
      node_api_create_object_with_properties(env, proto, names, values, width, &obj);
    }
    napi_set_element(env, result, i, obj);
    napi_close_handle_scope(env, scope);
  }
  free(descriptors);
  free(names);
  return result;
}

NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
  napi_value fn;
  napi_create_function(env, "make", NAPI_AUTO_LENGTH, Make, NULL, &fn);
  napi_set_named_property(env, exports, "make", fn);
  return exports;
}
{
  "targets": [
    {
      "target_name": "binding",
      "sources": ["binding.c"],
      "defines": ["NAPI_EXPERIMENTAL"]
    },
    {
      "target_name": "cached",
      "sources": ["cached.cc"],
      "cflags_cc": ["-std=c++20"],
      "xcode_settings": { "CLANG_CXX_LANGUAGE_STANDARD": "c++20" }
    }
  ]
}
'use strict';
// One case per process, so the cases do not share a heap:
//   for c in literal setters define proto null; do node --allow-natives-syntax repro.js $c 10; done
//   for c in today direct cached today-null cached-null; do node --allow-natives-syntax repro.js $c 10; done
const { make } = require('./build/Release/binding.node');
const { makeV8 } = require('./build/Release/cached.node');

const which = process.argv[2];
const width = Number(process.argv[3] ?? 10);
const count = 100_000;
const names = Array.from({ length: width }, (_, i) => `k${i}`);
const values = names.map((_, i) => i + 0.5);

// Every case stores the same values; only the way the objects are made differs.
const literals = new Function('n', 'v', `
  const a = new Array(n);
  for (let i = 0; i < n; i++) a[i] = { ${names.map((k, j) => `${k}: v[${j}]`).join(', ')} };
  return a;`);
const read = new Function('a', `
  let s = 0;
  for (let i = 0; i < a.length; i++) { const o = a[i]; s += ${names.map((k) => `o.${k}`).join(' + ')}; }
  return s;`);

const build = {
  // Node-API, binding.c
  literal: () => literals(count, values),
  setters: () => make(names, values, undefined, count, 1),
  define: () => make(names, values, undefined, count, 2),
  proto: () => make(names, values, Object.prototype, count),
  null: () => make(names, values, undefined, count),
  // V8 API, cached.cc: one harness for today's body, Object::New alone and the cache
  today: () => makeV8(names, values, Object.prototype, count, 0),
  direct: () => makeV8(names, values, Object.prototype, count, 1),
  cached: () => makeV8(names, values, Object.prototype, count, 2),
  'today-null': () => makeV8(names, values, undefined, count, 0),
  'cached-null': () => makeV8(names, values, undefined, count, 2),
}[which];

// Median ns per property of `runs` calls of f; returns the last result too.
function time(f, runs) {
  const times = [];
  let out;
  for (let i = 0; i < runs; i++) {
    const t0 = process.hrtime.bigint();
    out = f();
    times.push(Number(process.hrtime.bigint() - t0));
  }
  times.sort((x, y) => x - y);
  return [times[times.length >> 1] / (count * width), out];
}

build();
const [buildNs, objects] = time(build, 11);
for (let i = 0; i < 5; i++) read(objects);
const [readNs] = time(() => read(objects), 21);
const fast = %HasFastProperties(objects[0]);
const sameMap = %HaveSameMap(objects[0], objects[count - 1]);
console.log(
  `${process.version} ${which.padEnd(11)} ${width} properties: fast=${fast} sameMap=${sameMap} ` +
    `build ${buildNs.toFixed(1)} ns, read ${readNs.toFixed(2)} ns per property`,
);
# start 2026-10-01 10:48:03 10:48  up 11 days, 23:42, 5 users, load averages: 36.82 40.10 45.37
## round 1 10:48  up 11 days, 23:42, 5 users, load averages: 36.82 40.10 45.37
v26.10.0 literal     10 properties: fast=true sameMap=true build 4.1 ns, read 0.67 ns per property
v26.10.0 setters     10 properties: fast=true sameMap=true build 117.9 ns, read 0.83 ns per property
v26.10.0 define      10 properties: fast=true sameMap=true build 70.0 ns, read 0.76 ns per property
v26.10.0 proto       10 properties: fast=false sameMap=true build 34.0 ns, read 5.61 ns per property
v26.10.0 null        10 properties: fast=false sameMap=false build 45.0 ns, read 51.75 ns per property
v26.10.0 today       10 properties: fast=false sameMap=true build 28.6 ns, read 5.46 ns per property
v26.10.0 direct      10 properties: fast=false sameMap=true build 28.1 ns, read 5.73 ns per property
v26.10.0 cached      10 properties: fast=true sameMap=true build 27.4 ns, read 0.86 ns per property
v26.10.0 today-null  10 properties: fast=false sameMap=false build 35.8 ns, read 51.52 ns per property
v26.10.0 cached-null 10 properties: fast=true sameMap=true build 49.3 ns, read 0.70 ns per property
v26.10.0 literal     32 properties: fast=true sameMap=true build 3.8 ns, read 0.54 ns per property
v26.10.0 setters     32 properties: fast=false sameMap=true build 128.9 ns, read 5.83 ns per property
v26.10.0 define      32 properties: fast=true sameMap=true build 69.2 ns, read 0.67 ns per property
v26.10.0 proto       32 properties: fast=false sameMap=true build 27.4 ns, read 5.65 ns per property
v26.10.0 null        32 properties: fast=false sameMap=false build 29.1 ns, read 51.12 ns per property
v26.10.0 today       32 properties: fast=false sameMap=true build 26.8 ns, read 5.65 ns per property
v26.10.0 direct      32 properties: fast=false sameMap=true build 25.6 ns, read 6.77 ns per property
v26.10.0 cached      32 properties: fast=true sameMap=true build 20.5 ns, read 0.53 ns per property
v26.10.0 today-null  32 properties: fast=false sameMap=false build 26.1 ns, read 51.61 ns per property
v26.10.0 cached-null 32 properties: fast=true sameMap=true build 41.0 ns, read 0.51 ns per property
## round 2 10:48  up 11 days, 23:43, 5 users, load averages: 37.54 39.94 45.07
v26.10.0 literal     10 properties: fast=true sameMap=true build 3.9 ns, read 0.69 ns per property
v26.10.0 setters     10 properties: fast=true sameMap=true build 117.0 ns, read 0.76 ns per property
v26.10.0 define      10 properties: fast=true sameMap=true build 69.3 ns, read 0.75 ns per property
v26.10.0 proto       10 properties: fast=false sameMap=true build 32.9 ns, read 5.86 ns per property
v26.10.0 null        10 properties: fast=false sameMap=false build 46.4 ns, read 53.03 ns per property
v26.10.0 today       10 properties: fast=false sameMap=true build 29.8 ns, read 5.76 ns per property
v26.10.0 direct      10 properties: fast=false sameMap=true build 26.0 ns, read 5.76 ns per property
v26.10.0 cached      10 properties: fast=true sameMap=true build 29.4 ns, read 0.71 ns per property
v26.10.0 today-null  10 properties: fast=false sameMap=false build 37.6 ns, read 51.77 ns per property
v26.10.0 cached-null 10 properties: fast=true sameMap=true build 50.0 ns, read 0.71 ns per property
v26.10.0 literal     32 properties: fast=true sameMap=true build 3.7 ns, read 0.53 ns per property
v26.10.0 setters     32 properties: fast=false sameMap=true build 127.3 ns, read 7.76 ns per property
v26.10.0 define      32 properties: fast=true sameMap=true build 71.0 ns, read 0.83 ns per property
v26.10.0 proto       32 properties: fast=false sameMap=true build 26.5 ns, read 6.01 ns per property
v26.10.0 null        32 properties: fast=false sameMap=false build 28.0 ns, read 51.38 ns per property
v26.10.0 today       32 properties: fast=false sameMap=true build 26.4 ns, read 7.11 ns per property
v26.10.0 direct      32 properties: fast=false sameMap=true build 25.1 ns, read 5.79 ns per property
v26.10.0 cached      32 properties: fast=true sameMap=true build 20.4 ns, read 0.64 ns per property
v26.10.0 today-null  32 properties: fast=false sameMap=false build 27.5 ns, read 52.30 ns per property
v26.10.0 cached-null 32 properties: fast=true sameMap=true build 39.7 ns, read 0.53 ns per property
## round 3 10:49  up 11 days, 23:44, 5 users, load averages: 37.83 39.76 44.77
v26.10.0 cached-null 10 properties: fast=true sameMap=true build 49.0 ns, read 0.69 ns per property
v26.10.0 today-null  10 properties: fast=false sameMap=false build 34.9 ns, read 51.50 ns per property
v26.10.0 cached      10 properties: fast=true sameMap=true build 27.3 ns, read 0.70 ns per property
v26.10.0 direct      10 properties: fast=false sameMap=true build 27.1 ns, read 6.74 ns per property
v26.10.0 today       10 properties: fast=false sameMap=true build 30.3 ns, read 6.51 ns per property
v26.10.0 null        10 properties: fast=false sameMap=false build 39.4 ns, read 52.18 ns per property
v26.10.0 proto       10 properties: fast=false sameMap=true build 35.8 ns, read 5.63 ns per property
v26.10.0 define      10 properties: fast=true sameMap=true build 70.7 ns, read 0.82 ns per property
v26.10.0 setters     10 properties: fast=true sameMap=true build 116.6 ns, read 0.84 ns per property
v26.10.0 literal     10 properties: fast=true sameMap=true build 4.1 ns, read 0.70 ns per property
v26.10.0 cached-null 32 properties: fast=true sameMap=true build 41.0 ns, read 0.54 ns per property
v26.10.0 today-null  32 properties: fast=false sameMap=false build 29.0 ns, read 54.10 ns per property
v26.10.0 cached      32 properties: fast=true sameMap=true build 20.5 ns, read 0.52 ns per property
v26.10.0 direct      32 properties: fast=false sameMap=true build 24.5 ns, read 6.25 ns per property
v26.10.0 today       32 properties: fast=false sameMap=true build 26.9 ns, read 6.10 ns per property
v26.10.0 null        32 properties: fast=false sameMap=false build 30.2 ns, read 51.95 ns per property
v26.10.0 proto       32 properties: fast=false sameMap=true build 25.6 ns, read 5.78 ns per property
v26.10.0 define      32 properties: fast=true sameMap=true build 70.0 ns, read 0.74 ns per property
v26.10.0 setters     32 properties: fast=false sameMap=true build 127.2 ns, read 5.46 ns per property
v26.10.0 literal     32 properties: fast=true sameMap=true build 4.0 ns, read 0.53 ns per property
# end 2026-10-01 10:50:00 10:50  up 11 days, 23:44, 5 users, load averages: 37.90 39.55 44.46
# 2026-10-01 10:50:00 10:50  up 11 days, 23:44, 5 users, load averages: 37.90 39.55 44.46
$ for w in 10 32; do for c in literal setters define proto null; do node --allow-natives-syntax repro.js $c $w; done; done
v26.10.0 literal     10 properties: fast=true sameMap=true build 3.9 ns, read 0.70 ns per property
v26.10.0 setters     10 properties: fast=true sameMap=true build 120.1 ns, read 0.81 ns per property
v26.10.0 define      10 properties: fast=true sameMap=true build 82.5 ns, read 0.99 ns per property
v26.10.0 proto       10 properties: fast=false sameMap=true build 51.2 ns, read 6.94 ns per property
v26.10.0 null        10 properties: fast=false sameMap=false build 59.9 ns, read 52.70 ns per property
v26.10.0 literal     32 properties: fast=true sameMap=true build 4.0 ns, read 0.75 ns per property
v26.10.0 setters     32 properties: fast=false sameMap=true build 126.5 ns, read 6.83 ns per property
v26.10.0 define      32 properties: fast=true sameMap=true build 72.3 ns, read 0.86 ns per property
v26.10.0 proto       32 properties: fast=false sameMap=true build 44.0 ns, read 6.69 ns per property
v26.10.0 null        32 properties: fast=false sameMap=false build 30.9 ns, read 54.22 ns per property
A_EXIT=0
$ for w in 10 32; do for c in today direct cached today-null cached-null; do node --allow-natives-syntax repro.js $c $w; done; done
v26.10.0 today       10 properties: fast=false sameMap=true build 30.1 ns, read 6.94 ns per property
v26.10.0 direct      10 properties: fast=false sameMap=true build 29.8 ns, read 6.32 ns per property
v26.10.0 cached      10 properties: fast=true sameMap=true build 26.7 ns, read 0.71 ns per property
v26.10.0 today-null  10 properties: fast=false sameMap=false build 35.7 ns, read 51.41 ns per property
v26.10.0 cached-null 10 properties: fast=true sameMap=true build 48.1 ns, read 0.73 ns per property
v26.10.0 today       32 properties: fast=false sameMap=true build 23.4 ns, read 6.84 ns per property
v26.10.0 direct      32 properties: fast=false sameMap=true build 22.9 ns, read 5.54 ns per property
v26.10.0 cached      32 properties: fast=true sameMap=true build 20.9 ns, read 0.50 ns per property
v26.10.0 today-null  32 properties: fast=false sameMap=false build 26.2 ns, read 53.22 ns per property
v26.10.0 cached-null 32 properties: fast=true sameMap=true build 38.8 ns, read 0.57 ns per property
B_EXIT=0
# 2026-10-01 10:50:41 10:50  up 11 days, 23:45, 5 users, load averages: 38.41 39.55 44.24

napi-rs uses this call for every #[napi(object)] value when its experimental feature is on (napi-rs/napi-rs#3559), so all of those objects are affected.

What is the feature you are proposing to solve the problem?

Keep the API as it is, and have Node cache a v8::DictionaryTemplate per list of property names in the napi_env. When a list of names comes in a second time, create a template for it, and from then on create the objects with NewInstance. They come out fast and share one map. Node core already creates objects this way in a few places (#59802, #60033, #61432, and #66385 for node:sqlite rows).

I tried this in an addon against the V8 API (cached.cc below), next to a copy of today's implementation in the same harness. Same comparisons as above, with builds compared with today's implementation:

Fast properties Read, ns per property Build, ns per property
today, Object.prototype no 6.13 (8.8x) 30.0
cache, Object.prototype yes 0.71 (1.0x) 27.4 (0.91x)
today, NULL no, one map per object 51.51 (74x) 35.8 (1.2x)
cache, NULL yes 0.70 (1.0x) 49.1 (1.6x)

Reads go back to object-literal speed, and building gets a bit cheaper. With any prototype other than Object.prototype building gets more expensive, because each object then needs a SetPrototype (core's NewDictionaryInstanceNullProto does the same).

A real implementation would also have to handle:

  • DictionaryTemplate::New takes the names as one-byte strings, and it aborts on array-index names like "0". So symbols, two-byte names, index names and duplicates have to keep using Object::New.
  • From 128 properties V8 uses dictionary mode anyway.
  • A cache policy, so that addons with lots of different shapes don't thrash it. The cache in cached.cc is just a 256-entry direct-mapped table.
cached.cc
// The proposed body of node_api_create_object_with_properties, as an addon over the V8 API.
// makeV8(names, values, prototype, count, how) builds `count` objects like make() in binding.c,
// all in one harness so that only the object creation differs:
//   how 0: today's body (src/js_native_api_v8.cc: two LocalVector copies, then Object::New)
//   how 1: Object::New alone, without the copies
//   how 2: ShapeCache::New, a cache of v8::DictionaryTemplate keyed by the list of names; in Node
//          the cache would live in the napi_env, and ShapeCache::New is what the call would run
#include <node.h>
#include <v8.h>

#include <string>
#include <string_view>
#include <vector>

namespace {

// "0" .. "4294967294" in canonical form is an array index. DictionaryTemplate::New aborts on one.
bool IsArrayIndex(std::string_view s) {
  if (s.empty() || s.size() > 10 || (s.size() > 1 && s[0] == '0')) return false;
  uint64_t n = 0;
  for (char c : s) {
    if (c < '0' || c > '9') return false;
    n = n * 10 + static_cast<uint64_t>(c - '0');
  }
  return n <= 4294967294u;
}

class ShapeCache {
 public:
  explicit ShapeCache(v8::Isolate* isolate)
      : object_prototype_(isolate, v8::Object::New(isolate)->GetPrototypeV2()) {}

  v8::MaybeLocal<v8::Object> New(v8::Isolate* isolate,
                                 v8::Local<v8::Context> context,
                                 v8::Local<v8::Value> prototype,
                                 v8::Local<v8::Name>* names,
                                 v8::Local<v8::Value>* values,
                                 size_t count) {
    if (count > kMaxProperties) return v8::Object::New(isolate, prototype, names, values, count);
    // A string's identity hash is its content hash, so equal names created afresh still match.
    uint32_t hash = 0x9e3779b9u ^ static_cast<uint32_t>(count);
    for (size_t i = 0; i < count; i++)
      hash = hash * 31 + static_cast<uint32_t>(names[i]->GetIdentityHash());
    Entry& e = entries_[hash % kSize];
    bool hit = e.hash == hash && e.names.size() == count;
    for (size_t i = 0; hit && i < count; i++)
      hit = e.names[i] == names[i] || e.names[i].Get(isolate)->StrictEquals(names[i]);

    if (!hit) {
      // First sighting: remember the names, and build this object the way the call does today.
      e.hash = hash;
      e.slow = false;
      e.tmpl.Reset();
      e.names.clear();
      for (size_t i = 0; i < count; i++) e.names.emplace_back(isolate, names[i]);
    } else if (e.tmpl.IsEmpty() && !e.slow) {
      e.slow = !MakeTemplate(isolate, names, count, &e.tmpl);
    }
    if (!hit || e.slow)
      return v8::Object::New(isolate, prototype, names, values, count);

    v8::MaybeLocal<v8::Value> slots[kMaxProperties];
    for (size_t i = 0; i < count; i++) slots[i] = values[i];
    v8::Local<v8::Object> obj = e.tmpl.Get(isolate)->NewInstance(
        context, v8::MemorySpan<v8::MaybeLocal<v8::Value>>(slots, count));
    // The template's cached map has Object.prototype; any other prototype is one transition.
    if (object_prototype_ != prototype && obj->SetPrototypeV2(context, prototype).IsNothing())
      return {};
    return obj;
  }

 private:
  static constexpr size_t kSize = 256;  // direct-mapped, for the reproduction only
  // From 128 properties V8 gives dictionary mode to templates and object literals alike.
  static constexpr size_t kMaxProperties = 127;

  struct Entry {
    uint32_t hash = 0;
    std::vector<v8::Global<v8::Name>> names;
    bool slow = false;  // names DictionaryTemplate cannot hold
    v8::Global<v8::DictionaryTemplate> tmpl;
  };

  // DictionaryTemplate::New takes one-byte names and aborts on an array index; anything else,
  // symbols and duplicates included, keeps the Object::New path.
  static bool MakeTemplate(v8::Isolate* isolate,
                           v8::Local<v8::Name>* names,
                           size_t count,
                           v8::Global<v8::DictionaryTemplate>* out) {
    std::vector<std::string> bytes(count);
    for (size_t i = 0; i < count; i++) {
      if (!names[i]->IsString()) return false;
      v8::Local<v8::String> s = names[i].As<v8::String>();
      if (!s->ContainsOnlyOneByte()) return false;
      bytes[i].resize(s->Length());
      s->WriteOneByteV2(isolate, 0, s->Length(), reinterpret_cast<uint8_t*>(bytes[i].data()));
      if (IsArrayIndex(bytes[i])) return false;
      for (size_t j = 0; j < i; j++)
        if (bytes[j] == bytes[i]) return false;
    }
    std::vector<std::string_view> views(bytes.begin(), bytes.end());
    out->Reset(isolate,
               v8::DictionaryTemplate::New(
                   isolate, v8::MemorySpan<const std::string_view>(views.data(), views.size())));
    return true;
  }

  v8::Global<v8::Value> object_prototype_;
  Entry entries_[kSize];
};

// Today's body of node_api_create_object_with_properties, from the copies on.
v8::MaybeLocal<v8::Object> Today(v8::Isolate* isolate,
                                 v8::Local<v8::Value> prototype,
                                 v8::Local<v8::Name>* names,
                                 v8::Local<v8::Value>* values,
                                 size_t count) {
  v8::LocalVector<v8::Name> v8_names(isolate, count);
  v8::LocalVector<v8::Value> v8_values(isolate, count);
  for (size_t i = 0; i < count; i++) {
    v8_names[i] = names[i];
    v8_values[i] = values[i];
  }
  return v8::Object::New(isolate, prototype, v8_names.data(), v8_values.data(), count);
}

void MakeV8(const v8::FunctionCallbackInfo<v8::Value>& info) {
  v8::Isolate* isolate = info.GetIsolate();
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  auto* cache = static_cast<ShapeCache*>(
      info.Data().As<v8::External>()->Value(v8::kExternalPointerTypeTagDefault));
  v8::Local<v8::Array> names_in = info[0].As<v8::Array>();
  v8::Local<v8::Array> values_in = info[1].As<v8::Array>();
  // An undefined prototype stands for NULL, which the call turns into null.
  v8::Local<v8::Value> prototype =
      info[2]->IsUndefined() ? v8::Local<v8::Value>(v8::Null(isolate)) : info[2];
  uint32_t width = names_in->Length();
  uint32_t count = info[3].As<v8::Uint32>()->Value();
  uint32_t how = info[4].As<v8::Uint32>()->Value();

  v8::LocalVector<v8::Name> names(isolate, width);
  v8::LocalVector<v8::Value> values(isolate, width);
  for (uint32_t i = 0; i < width; i++) {
    names[i] = names_in->Get(context, i).ToLocalChecked().As<v8::Name>();
    values[i] = values_in->Get(context, i).ToLocalChecked();
  }
  v8::Local<v8::Array> result = v8::Array::New(isolate, count);
  for (uint32_t i = 0; i < count; i++) {
    v8::HandleScope scope(isolate);
    v8::MaybeLocal<v8::Object> made =
        how == 0   ? Today(isolate, prototype, names.data(), values.data(), width)
        : how == 1 ? v8::MaybeLocal<v8::Object>(v8::Object::New(
                         isolate, prototype, names.data(), values.data(), width))
                   : cache->New(isolate, context, prototype, names.data(), values.data(), width);
    v8::Local<v8::Object> obj;
    if (!made.ToLocal(&obj)) return;
    result->Set(context, i, obj).Check();
  }
  info.GetReturnValue().Set(result);
}

}  // namespace

NODE_MODULE_INIT(/* exports, module, context */) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  auto* cache = new ShapeCache(isolate);
  node::AddEnvironmentCleanupHook(
      isolate, [](void* arg) { delete static_cast<ShapeCache*>(arg); }, cache);
  v8::Local<v8::External> data =
      v8::External::New(isolate, cache, v8::kExternalPointerTypeTagDefault);
  exports
      ->Set(context, v8::String::NewFromUtf8Literal(isolate, "makeV8"),
            v8::FunctionTemplate::New(isolate, MakeV8, data)
                ->GetFunction(context)
                .ToLocalChecked())
      .Check();
}

What alternatives have you considered?

An explicit template API: create a template from a list of names, create objects from it, delete it. That avoids hashing the names on every call and needs no cache policy, but it's a new API, and it only helps addons that switch to it.

Changing V8 so that Object::New returns fast objects would be another way, but dictionary mode is how that API is designed.

Would the team prefer the cache inside the existing call, or a template API? I can open a PR for either.


AI disclosure: I used a coding agent for the investigation, the reproduction and the text of this issue. I checked the results and will answer comments myself.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues requesting new Node.js features.node-apiIssues and PRs related to Node-API.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions