René's URL Explorer Experiment


Title: node-api: objects from node_api_create_object_with_properties are in dictionary mode · Issue #66441 · nodejs/node · GitHub

Open Graph Title: node-api: objects from node_api_create_object_with_properties are in dictionary mode · Issue #66441 · nodejs/node

X Title: node-api: objects from node_api_create_object_with_properties are in dictionary mode · Issue #66441 · nodejs/node

Description: 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 dictionar...

Open Graph Description: 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, v...

X Description: 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, v...

Opengraph URL: https://github.com/nodejs/node/issues/66441

X: @github

direct link

Domain: github.com


Hey, it has json ld scripts:
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"node-api: objects from node_api_create_object_with_properties are in dictionary mode","articleBody":"### What is the problem this feature will solve?\n\nThe 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.\n\n`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.\n\nI 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`.\n\n|  | Fast properties | Read, ns per property | Build, ns per property |\n| --- | --- | --- | --- |\n| object literal | yes | 0.69 | 4.0 |\n| `napi_set_property` per property | yes, up to 19 properties | 0.82 (1.2x) | 117.5 (3.4x) |\n| `napi_define_properties` | yes | 0.79 (1.1x) | 70.3 (2.0x) |\n| `node_api_create_object_with_properties`, `Object.prototype` | **no** | **5.75 (8.3x)** | 34.9 |\n| `node_api_create_object_with_properties`, `NULL` | **no**, one map per object | **52.44 (75x)** | 45.7 (1.3x) |\n\nAt 32 properties it looks the same, except that the `napi_set_property` objects are in dictionary mode too.\n\n\u003cdetails>\u003csummary>Repro and raw output\u003c/summary>\n\n```c\n#include \u003cnode_api.h>\n#include \u003cstdlib.h>\n\nenum { kWithProperties, kSetProperty, kDefineProperties };\n\n// make(names, values, prototype, count[, how]): `count` objects, each from one\n// node_api_create_object_with_properties call (an undefined prototype passes NULL), or with\n// `how` 1 from napi_create_object and one napi_set_property call per property, or with `how` 2\n// from napi_create_object and one napi_define_properties call (the prototype is ignored for both).\nstatic napi_value Make(napi_env env, napi_callback_info info) {\n  size_t argc = 5;\n  napi_value argv[5], result, proto;\n  uint32_t width, count;\n  int32_t how = kWithProperties;\n  napi_valuetype type;\n  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);\n  napi_get_array_length(env, argv[0], &width);\n  napi_get_value_uint32(env, argv[3], &count);\n  napi_typeof(env, argv[2], &type);\n  proto = type == napi_undefined ? NULL : argv[2];\n  if (argc > 4) napi_get_value_int32(env, argv[4], &how);\n\n  napi_value* names = malloc(sizeof(napi_value) * width * 2);\n  napi_value* values = names + width;\n  napi_property_descriptor* descriptors = calloc(width, sizeof(napi_property_descriptor));\n  for (uint32_t i = 0; i \u003c width; i++) {\n    napi_get_element(env, argv[0], i, &names[i]);\n    napi_get_element(env, argv[1], i, &values[i]);\n    descriptors[i].name = names[i];\n    descriptors[i].value = values[i];\n    descriptors[i].attributes = napi_default_jsproperty;\n  }\n\n  napi_create_array_with_length(env, count, &result);\n  for (uint32_t i = 0; i \u003c count; i++) {\n    napi_handle_scope scope;\n    napi_value obj;\n    napi_open_handle_scope(env, &scope);\n    if (how == kSetProperty) {\n      napi_create_object(env, &obj);\n      for (uint32_t j = 0; j \u003c width; j++) napi_set_property(env, obj, names[j], values[j]);\n    } else if (how == kDefineProperties) {\n      napi_create_object(env, &obj);\n      napi_define_properties(env, obj, width, descriptors);\n    } else {\n      node_api_create_object_with_properties(env, proto, names, values, width, &obj);\n    }\n    napi_set_element(env, result, i, obj);\n    napi_close_handle_scope(env, scope);\n  }\n  free(descriptors);\n  free(names);\n  return result;\n}\n\nNAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {\n  napi_value fn;\n  napi_create_function(env, \"make\", NAPI_AUTO_LENGTH, Make, NULL, &fn);\n  napi_set_named_property(env, exports, \"make\", fn);\n  return exports;\n}\n```\n\n```json\n{\n  \"targets\": [\n    {\n      \"target_name\": \"binding\",\n      \"sources\": [\"binding.c\"],\n      \"defines\": [\"NAPI_EXPERIMENTAL\"]\n    },\n    {\n      \"target_name\": \"cached\",\n      \"sources\": [\"cached.cc\"],\n      \"cflags_cc\": [\"-std=c++20\"],\n      \"xcode_settings\": { \"CLANG_CXX_LANGUAGE_STANDARD\": \"c++20\" }\n    }\n  ]\n}\n```\n\n```js\n'use strict';\n// One case per process, so the cases do not share a heap:\n//   for c in literal setters define proto null; do node --allow-natives-syntax repro.js $c 10; done\n//   for c in today direct cached today-null cached-null; do node --allow-natives-syntax repro.js $c 10; done\nconst { make } = require('./build/Release/binding.node');\nconst { makeV8 } = require('./build/Release/cached.node');\n\nconst which = process.argv[2];\nconst width = Number(process.argv[3] ?? 10);\nconst count = 100_000;\nconst names = Array.from({ length: width }, (_, i) => `k${i}`);\nconst values = names.map((_, i) => i + 0.5);\n\n// Every case stores the same values; only the way the objects are made differs.\nconst literals = new Function('n', 'v', `\n  const a = new Array(n);\n  for (let i = 0; i \u003c n; i++) a[i] = { ${names.map((k, j) => `${k}: v[${j}]`).join(', ')} };\n  return a;`);\nconst read = new Function('a', `\n  let s = 0;\n  for (let i = 0; i \u003c a.length; i++) { const o = a[i]; s += ${names.map((k) => `o.${k}`).join(' + ')}; }\n  return s;`);\n\nconst build = {\n  // Node-API, binding.c\n  literal: () => literals(count, values),\n  setters: () => make(names, values, undefined, count, 1),\n  define: () => make(names, values, undefined, count, 2),\n  proto: () => make(names, values, Object.prototype, count),\n  null: () => make(names, values, undefined, count),\n  // V8 API, cached.cc: one harness for today's body, Object::New alone and the cache\n  today: () => makeV8(names, values, Object.prototype, count, 0),\n  direct: () => makeV8(names, values, Object.prototype, count, 1),\n  cached: () => makeV8(names, values, Object.prototype, count, 2),\n  'today-null': () => makeV8(names, values, undefined, count, 0),\n  'cached-null': () => makeV8(names, values, undefined, count, 2),\n}[which];\n\n// Median ns per property of `runs` calls of f; returns the last result too.\nfunction time(f, runs) {\n  const times = [];\n  let out;\n  for (let i = 0; i \u003c runs; i++) {\n    const t0 = process.hrtime.bigint();\n    out = f();\n    times.push(Number(process.hrtime.bigint() - t0));\n  }\n  times.sort((x, y) => x - y);\n  return [times[times.length >> 1] / (count * width), out];\n}\n\nbuild();\nconst [buildNs, objects] = time(build, 11);\nfor (let i = 0; i \u003c 5; i++) read(objects);\nconst [readNs] = time(() => read(objects), 21);\nconst fast = %HasFastProperties(objects[0]);\nconst sameMap = %HaveSameMap(objects[0], objects[count - 1]);\nconsole.log(\n  `${process.version} ${which.padEnd(11)} ${width} properties: fast=${fast} sameMap=${sameMap} ` +\n    `build ${buildNs.toFixed(1)} ns, read ${readNs.toFixed(2)} ns per property`,\n);\n```\n\n```\n# start 2026-10-01 10:48:03 10:48  up 11 days, 23:42, 5 users, load averages: 36.82 40.10 45.37\n## round 1 10:48  up 11 days, 23:42, 5 users, load averages: 36.82 40.10 45.37\nv26.10.0 literal     10 properties: fast=true sameMap=true build 4.1 ns, read 0.67 ns per property\nv26.10.0 setters     10 properties: fast=true sameMap=true build 117.9 ns, read 0.83 ns per property\nv26.10.0 define      10 properties: fast=true sameMap=true build 70.0 ns, read 0.76 ns per property\nv26.10.0 proto       10 properties: fast=false sameMap=true build 34.0 ns, read 5.61 ns per property\nv26.10.0 null        10 properties: fast=false sameMap=false build 45.0 ns, read 51.75 ns per property\nv26.10.0 today       10 properties: fast=false sameMap=true build 28.6 ns, read 5.46 ns per property\nv26.10.0 direct      10 properties: fast=false sameMap=true build 28.1 ns, read 5.73 ns per property\nv26.10.0 cached      10 properties: fast=true sameMap=true build 27.4 ns, read 0.86 ns per property\nv26.10.0 today-null  10 properties: fast=false sameMap=false build 35.8 ns, read 51.52 ns per property\nv26.10.0 cached-null 10 properties: fast=true sameMap=true build 49.3 ns, read 0.70 ns per property\nv26.10.0 literal     32 properties: fast=true sameMap=true build 3.8 ns, read 0.54 ns per property\nv26.10.0 setters     32 properties: fast=false sameMap=true build 128.9 ns, read 5.83 ns per property\nv26.10.0 define      32 properties: fast=true sameMap=true build 69.2 ns, read 0.67 ns per property\nv26.10.0 proto       32 properties: fast=false sameMap=true build 27.4 ns, read 5.65 ns per property\nv26.10.0 null        32 properties: fast=false sameMap=false build 29.1 ns, read 51.12 ns per property\nv26.10.0 today       32 properties: fast=false sameMap=true build 26.8 ns, read 5.65 ns per property\nv26.10.0 direct      32 properties: fast=false sameMap=true build 25.6 ns, read 6.77 ns per property\nv26.10.0 cached      32 properties: fast=true sameMap=true build 20.5 ns, read 0.53 ns per property\nv26.10.0 today-null  32 properties: fast=false sameMap=false build 26.1 ns, read 51.61 ns per property\nv26.10.0 cached-null 32 properties: fast=true sameMap=true build 41.0 ns, read 0.51 ns per property\n## round 2 10:48  up 11 days, 23:43, 5 users, load averages: 37.54 39.94 45.07\nv26.10.0 literal     10 properties: fast=true sameMap=true build 3.9 ns, read 0.69 ns per property\nv26.10.0 setters     10 properties: fast=true sameMap=true build 117.0 ns, read 0.76 ns per property\nv26.10.0 define      10 properties: fast=true sameMap=true build 69.3 ns, read 0.75 ns per property\nv26.10.0 proto       10 properties: fast=false sameMap=true build 32.9 ns, read 5.86 ns per property\nv26.10.0 null        10 properties: fast=false sameMap=false build 46.4 ns, read 53.03 ns per property\nv26.10.0 today       10 properties: fast=false sameMap=true build 29.8 ns, read 5.76 ns per property\nv26.10.0 direct      10 properties: fast=false sameMap=true build 26.0 ns, read 5.76 ns per property\nv26.10.0 cached      10 properties: fast=true sameMap=true build 29.4 ns, read 0.71 ns per property\nv26.10.0 today-null  10 properties: fast=false sameMap=false build 37.6 ns, read 51.77 ns per property\nv26.10.0 cached-null 10 properties: fast=true sameMap=true build 50.0 ns, read 0.71 ns per property\nv26.10.0 literal     32 properties: fast=true sameMap=true build 3.7 ns, read 0.53 ns per property\nv26.10.0 setters     32 properties: fast=false sameMap=true build 127.3 ns, read 7.76 ns per property\nv26.10.0 define      32 properties: fast=true sameMap=true build 71.0 ns, read 0.83 ns per property\nv26.10.0 proto       32 properties: fast=false sameMap=true build 26.5 ns, read 6.01 ns per property\nv26.10.0 null        32 properties: fast=false sameMap=false build 28.0 ns, read 51.38 ns per property\nv26.10.0 today       32 properties: fast=false sameMap=true build 26.4 ns, read 7.11 ns per property\nv26.10.0 direct      32 properties: fast=false sameMap=true build 25.1 ns, read 5.79 ns per property\nv26.10.0 cached      32 properties: fast=true sameMap=true build 20.4 ns, read 0.64 ns per property\nv26.10.0 today-null  32 properties: fast=false sameMap=false build 27.5 ns, read 52.30 ns per property\nv26.10.0 cached-null 32 properties: fast=true sameMap=true build 39.7 ns, read 0.53 ns per property\n## round 3 10:49  up 11 days, 23:44, 5 users, load averages: 37.83 39.76 44.77\nv26.10.0 cached-null 10 properties: fast=true sameMap=true build 49.0 ns, read 0.69 ns per property\nv26.10.0 today-null  10 properties: fast=false sameMap=false build 34.9 ns, read 51.50 ns per property\nv26.10.0 cached      10 properties: fast=true sameMap=true build 27.3 ns, read 0.70 ns per property\nv26.10.0 direct      10 properties: fast=false sameMap=true build 27.1 ns, read 6.74 ns per property\nv26.10.0 today       10 properties: fast=false sameMap=true build 30.3 ns, read 6.51 ns per property\nv26.10.0 null        10 properties: fast=false sameMap=false build 39.4 ns, read 52.18 ns per property\nv26.10.0 proto       10 properties: fast=false sameMap=true build 35.8 ns, read 5.63 ns per property\nv26.10.0 define      10 properties: fast=true sameMap=true build 70.7 ns, read 0.82 ns per property\nv26.10.0 setters     10 properties: fast=true sameMap=true build 116.6 ns, read 0.84 ns per property\nv26.10.0 literal     10 properties: fast=true sameMap=true build 4.1 ns, read 0.70 ns per property\nv26.10.0 cached-null 32 properties: fast=true sameMap=true build 41.0 ns, read 0.54 ns per property\nv26.10.0 today-null  32 properties: fast=false sameMap=false build 29.0 ns, read 54.10 ns per property\nv26.10.0 cached      32 properties: fast=true sameMap=true build 20.5 ns, read 0.52 ns per property\nv26.10.0 direct      32 properties: fast=false sameMap=true build 24.5 ns, read 6.25 ns per property\nv26.10.0 today       32 properties: fast=false sameMap=true build 26.9 ns, read 6.10 ns per property\nv26.10.0 null        32 properties: fast=false sameMap=false build 30.2 ns, read 51.95 ns per property\nv26.10.0 proto       32 properties: fast=false sameMap=true build 25.6 ns, read 5.78 ns per property\nv26.10.0 define      32 properties: fast=true sameMap=true build 70.0 ns, read 0.74 ns per property\nv26.10.0 setters     32 properties: fast=false sameMap=true build 127.2 ns, read 5.46 ns per property\nv26.10.0 literal     32 properties: fast=true sameMap=true build 4.0 ns, read 0.53 ns per property\n# end 2026-10-01 10:50:00 10:50  up 11 days, 23:44, 5 users, load averages: 37.90 39.55 44.46\n# 2026-10-01 10:50:00 10:50  up 11 days, 23:44, 5 users, load averages: 37.90 39.55 44.46\n$ 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\nv26.10.0 literal     10 properties: fast=true sameMap=true build 3.9 ns, read 0.70 ns per property\nv26.10.0 setters     10 properties: fast=true sameMap=true build 120.1 ns, read 0.81 ns per property\nv26.10.0 define      10 properties: fast=true sameMap=true build 82.5 ns, read 0.99 ns per property\nv26.10.0 proto       10 properties: fast=false sameMap=true build 51.2 ns, read 6.94 ns per property\nv26.10.0 null        10 properties: fast=false sameMap=false build 59.9 ns, read 52.70 ns per property\nv26.10.0 literal     32 properties: fast=true sameMap=true build 4.0 ns, read 0.75 ns per property\nv26.10.0 setters     32 properties: fast=false sameMap=true build 126.5 ns, read 6.83 ns per property\nv26.10.0 define      32 properties: fast=true sameMap=true build 72.3 ns, read 0.86 ns per property\nv26.10.0 proto       32 properties: fast=false sameMap=true build 44.0 ns, read 6.69 ns per property\nv26.10.0 null        32 properties: fast=false sameMap=false build 30.9 ns, read 54.22 ns per property\nA_EXIT=0\n$ 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\nv26.10.0 today       10 properties: fast=false sameMap=true build 30.1 ns, read 6.94 ns per property\nv26.10.0 direct      10 properties: fast=false sameMap=true build 29.8 ns, read 6.32 ns per property\nv26.10.0 cached      10 properties: fast=true sameMap=true build 26.7 ns, read 0.71 ns per property\nv26.10.0 today-null  10 properties: fast=false sameMap=false build 35.7 ns, read 51.41 ns per property\nv26.10.0 cached-null 10 properties: fast=true sameMap=true build 48.1 ns, read 0.73 ns per property\nv26.10.0 today       32 properties: fast=false sameMap=true build 23.4 ns, read 6.84 ns per property\nv26.10.0 direct      32 properties: fast=false sameMap=true build 22.9 ns, read 5.54 ns per property\nv26.10.0 cached      32 properties: fast=true sameMap=true build 20.9 ns, read 0.50 ns per property\nv26.10.0 today-null  32 properties: fast=false sameMap=false build 26.2 ns, read 53.22 ns per property\nv26.10.0 cached-null 32 properties: fast=true sameMap=true build 38.8 ns, read 0.57 ns per property\nB_EXIT=0\n# 2026-10-01 10:50:41 10:50  up 11 days, 23:45, 5 users, load averages: 38.41 39.55 44.24\n```\n\n\u003c/details>\n\nnapi-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.\n\n### What is the feature you are proposing to solve the problem?\n\nKeep 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).\n\nI 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:\n\n|  | Fast properties | Read, ns per property | Build, ns per property |\n| --- | --- | --- | --- |\n| today, `Object.prototype` | no | 6.13 (8.8x) | 30.0 |\n| cache, `Object.prototype` | **yes** | **0.71 (1.0x)** | 27.4 (0.91x) |\n| today, `NULL` | no, one map per object | 51.51 (74x) | 35.8 (1.2x) |\n| cache, `NULL` | **yes** | **0.70 (1.0x)** | 49.1 (1.6x) |\n\nReads 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).\n\nA real implementation would also have to handle:\n\n- `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`.\n- From 128 properties V8 uses dictionary mode anyway.\n- 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.\n\n\u003cdetails>\u003csummary>cached.cc\u003c/summary>\n\n```cpp\n// The proposed body of node_api_create_object_with_properties, as an addon over the V8 API.\n// makeV8(names, values, prototype, count, how) builds `count` objects like make() in binding.c,\n// all in one harness so that only the object creation differs:\n//   how 0: today's body (src/js_native_api_v8.cc: two LocalVector copies, then Object::New)\n//   how 1: Object::New alone, without the copies\n//   how 2: ShapeCache::New, a cache of v8::DictionaryTemplate keyed by the list of names; in Node\n//          the cache would live in the napi_env, and ShapeCache::New is what the call would run\n#include \u003cnode.h>\n#include \u003cv8.h>\n\n#include \u003cstring>\n#include \u003cstring_view>\n#include \u003cvector>\n\nnamespace {\n\n// \"0\" .. \"4294967294\" in canonical form is an array index. DictionaryTemplate::New aborts on one.\nbool IsArrayIndex(std::string_view s) {\n  if (s.empty() || s.size() > 10 || (s.size() > 1 && s[0] == '0')) return false;\n  uint64_t n = 0;\n  for (char c : s) {\n    if (c \u003c '0' || c > '9') return false;\n    n = n * 10 + static_cast\u003cuint64_t>(c - '0');\n  }\n  return n \u003c= 4294967294u;\n}\n\nclass ShapeCache {\n public:\n  explicit ShapeCache(v8::Isolate* isolate)\n      : object_prototype_(isolate, v8::Object::New(isolate)->GetPrototypeV2()) {}\n\n  v8::MaybeLocal\u003cv8::Object> New(v8::Isolate* isolate,\n                                 v8::Local\u003cv8::Context> context,\n                                 v8::Local\u003cv8::Value> prototype,\n                                 v8::Local\u003cv8::Name>* names,\n                                 v8::Local\u003cv8::Value>* values,\n                                 size_t count) {\n    if (count > kMaxProperties) return v8::Object::New(isolate, prototype, names, values, count);\n    // A string's identity hash is its content hash, so equal names created afresh still match.\n    uint32_t hash = 0x9e3779b9u ^ static_cast\u003cuint32_t>(count);\n    for (size_t i = 0; i \u003c count; i++)\n      hash = hash * 31 + static_cast\u003cuint32_t>(names[i]->GetIdentityHash());\n    Entry& e = entries_[hash % kSize];\n    bool hit = e.hash == hash && e.names.size() == count;\n    for (size_t i = 0; hit && i \u003c count; i++)\n      hit = e.names[i] == names[i] || e.names[i].Get(isolate)->StrictEquals(names[i]);\n\n    if (!hit) {\n      // First sighting: remember the names, and build this object the way the call does today.\n      e.hash = hash;\n      e.slow = false;\n      e.tmpl.Reset();\n      e.names.clear();\n      for (size_t i = 0; i \u003c count; i++) e.names.emplace_back(isolate, names[i]);\n    } else if (e.tmpl.IsEmpty() && !e.slow) {\n      e.slow = !MakeTemplate(isolate, names, count, &e.tmpl);\n    }\n    if (!hit || e.slow)\n      return v8::Object::New(isolate, prototype, names, values, count);\n\n    v8::MaybeLocal\u003cv8::Value> slots[kMaxProperties];\n    for (size_t i = 0; i \u003c count; i++) slots[i] = values[i];\n    v8::Local\u003cv8::Object> obj = e.tmpl.Get(isolate)->NewInstance(\n        context, v8::MemorySpan\u003cv8::MaybeLocal\u003cv8::Value>>(slots, count));\n    // The template's cached map has Object.prototype; any other prototype is one transition.\n    if (object_prototype_ != prototype && obj->SetPrototypeV2(context, prototype).IsNothing())\n      return {};\n    return obj;\n  }\n\n private:\n  static constexpr size_t kSize = 256;  // direct-mapped, for the reproduction only\n  // From 128 properties V8 gives dictionary mode to templates and object literals alike.\n  static constexpr size_t kMaxProperties = 127;\n\n  struct Entry {\n    uint32_t hash = 0;\n    std::vector\u003cv8::Global\u003cv8::Name>> names;\n    bool slow = false;  // names DictionaryTemplate cannot hold\n    v8::Global\u003cv8::DictionaryTemplate> tmpl;\n  };\n\n  // DictionaryTemplate::New takes one-byte names and aborts on an array index; anything else,\n  // symbols and duplicates included, keeps the Object::New path.\n  static bool MakeTemplate(v8::Isolate* isolate,\n                           v8::Local\u003cv8::Name>* names,\n                           size_t count,\n                           v8::Global\u003cv8::DictionaryTemplate>* out) {\n    std::vector\u003cstd::string> bytes(count);\n    for (size_t i = 0; i \u003c count; i++) {\n      if (!names[i]->IsString()) return false;\n      v8::Local\u003cv8::String> s = names[i].As\u003cv8::String>();\n      if (!s->ContainsOnlyOneByte()) return false;\n      bytes[i].resize(s->Length());\n      s->WriteOneByteV2(isolate, 0, s->Length(), reinterpret_cast\u003cuint8_t*>(bytes[i].data()));\n      if (IsArrayIndex(bytes[i])) return false;\n      for (size_t j = 0; j \u003c i; j++)\n        if (bytes[j] == bytes[i]) return false;\n    }\n    std::vector\u003cstd::string_view> views(bytes.begin(), bytes.end());\n    out->Reset(isolate,\n               v8::DictionaryTemplate::New(\n                   isolate, v8::MemorySpan\u003cconst std::string_view>(views.data(), views.size())));\n    return true;\n  }\n\n  v8::Global\u003cv8::Value> object_prototype_;\n  Entry entries_[kSize];\n};\n\n// Today's body of node_api_create_object_with_properties, from the copies on.\nv8::MaybeLocal\u003cv8::Object> Today(v8::Isolate* isolate,\n                                 v8::Local\u003cv8::Value> prototype,\n                                 v8::Local\u003cv8::Name>* names,\n                                 v8::Local\u003cv8::Value>* values,\n                                 size_t count) {\n  v8::LocalVector\u003cv8::Name> v8_names(isolate, count);\n  v8::LocalVector\u003cv8::Value> v8_values(isolate, count);\n  for (size_t i = 0; i \u003c count; i++) {\n    v8_names[i] = names[i];\n    v8_values[i] = values[i];\n  }\n  return v8::Object::New(isolate, prototype, v8_names.data(), v8_values.data(), count);\n}\n\nvoid MakeV8(const v8::FunctionCallbackInfo\u003cv8::Value>& info) {\n  v8::Isolate* isolate = info.GetIsolate();\n  v8::Local\u003cv8::Context> context = isolate->GetCurrentContext();\n  auto* cache = static_cast\u003cShapeCache*>(\n      info.Data().As\u003cv8::External>()->Value(v8::kExternalPointerTypeTagDefault));\n  v8::Local\u003cv8::Array> names_in = info[0].As\u003cv8::Array>();\n  v8::Local\u003cv8::Array> values_in = info[1].As\u003cv8::Array>();\n  // An undefined prototype stands for NULL, which the call turns into null.\n  v8::Local\u003cv8::Value> prototype =\n      info[2]->IsUndefined() ? v8::Local\u003cv8::Value>(v8::Null(isolate)) : info[2];\n  uint32_t width = names_in->Length();\n  uint32_t count = info[3].As\u003cv8::Uint32>()->Value();\n  uint32_t how = info[4].As\u003cv8::Uint32>()->Value();\n\n  v8::LocalVector\u003cv8::Name> names(isolate, width);\n  v8::LocalVector\u003cv8::Value> values(isolate, width);\n  for (uint32_t i = 0; i \u003c width; i++) {\n    names[i] = names_in->Get(context, i).ToLocalChecked().As\u003cv8::Name>();\n    values[i] = values_in->Get(context, i).ToLocalChecked();\n  }\n  v8::Local\u003cv8::Array> result = v8::Array::New(isolate, count);\n  for (uint32_t i = 0; i \u003c count; i++) {\n    v8::HandleScope scope(isolate);\n    v8::MaybeLocal\u003cv8::Object> made =\n        how == 0   ? Today(isolate, prototype, names.data(), values.data(), width)\n        : how == 1 ? v8::MaybeLocal\u003cv8::Object>(v8::Object::New(\n                         isolate, prototype, names.data(), values.data(), width))\n                   : cache->New(isolate, context, prototype, names.data(), values.data(), width);\n    v8::Local\u003cv8::Object> obj;\n    if (!made.ToLocal(&obj)) return;\n    result->Set(context, i, obj).Check();\n  }\n  info.GetReturnValue().Set(result);\n}\n\n}  // namespace\n\nNODE_MODULE_INIT(/* exports, module, context */) {\n  v8::Isolate* isolate = v8::Isolate::GetCurrent();\n  auto* cache = new ShapeCache(isolate);\n  node::AddEnvironmentCleanupHook(\n      isolate, [](void* arg) { delete static_cast\u003cShapeCache*>(arg); }, cache);\n  v8::Local\u003cv8::External> data =\n      v8::External::New(isolate, cache, v8::kExternalPointerTypeTagDefault);\n  exports\n      ->Set(context, v8::String::NewFromUtf8Literal(isolate, \"makeV8\"),\n            v8::FunctionTemplate::New(isolate, MakeV8, data)\n                ->GetFunction(context)\n                .ToLocalChecked())\n      .Check();\n}\n```\n\n\u003c/details>\n\n### What alternatives have you considered?\n\nAn 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.\n\nChanging V8 so that `Object::New` returns fast objects would be another way, but dictionary mode is how that API is designed.\n\nWould the team prefer the cache inside the existing call, or a template API? I can open a PR for either.\n\n---\n\nAI 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.\n","author":{"url":"https://github.com/colinhacks","@type":"Person","name":"colinhacks"},"datePublished":"2026-10-01T18:16:55Z","url":"https://github.com/nodejs/node/issues/66441"}

react-profiling0
react-import-mapreact,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf
route-pattern/_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format)
route-controllervoltron_issues_fragments
route-actionissue_layout
fetch-noncev2:072f3d00-7929-3cbb-eb36-e32dbd0e3c91
current-catalog-service-hash81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114
request-idD24A:3C4A99:655140:83A44C:6AED2BE3
html-safe-nonceb349b115e8d5684357ddfa6c646275ef185349624c5c3cd1d94496b75d68447f
visitor-payloadeyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEMjRBOjNDNEE5OTo2NTUxNDA6ODNBNDRDOjZBRUQyQkUzIiwidmlzaXRvcl9pZCI6IjI0MzA1ODc3NzYzMTk0MzM2OTkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ==
visitor-hmaccd701f41f7fb2a8394e2bacfa8b23c7a59844a0ff93144f6157859574240f85a
hovercard-subject-tagissue:5666961551
github-keyboard-shortcutsrepository,issues,copilot
google-site-verificationApib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I
octolytics-urlhttps://collector.github.com/github/collect
analytics-location///voltron/issues_fragments/issue_layout
fb:app_id1401488693436528
apple-itunes-appapp-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/66441/issue_layout
twitter:imagehttps://opengraph.githubassets.com/4d6a8cc3808837e10a86a25aaa2fa7f62d322baebb5e7d68afc05966c163acea/nodejs/node/issues/66441
twitter:cardsummary_large_image
og:imagehttps://opengraph.githubassets.com/4d6a8cc3808837e10a86a25aaa2fa7f62d322baebb5e7d68afc05966c163acea/nodejs/node/issues/66441
og:image:altWhat 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, v...
og:image:width1200
og:image:height600
og:site_nameGitHub
og:typeobject
og:author:usernamecolinhacks
hostnamegithub.com
expected-hostnamegithub.com
None9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2
turbo-cache-controlno-preview
go-importgithub.com/nodejs/node git https://github.com/nodejs/node.git
octolytics-dimension-user_id9950313
octolytics-dimension-user_loginnodejs
octolytics-dimension-repository_id27193779
octolytics-dimension-repository_nwonodejs/node
octolytics-dimension-repository_publictrue
octolytics-dimension-repository_is_forkfalse
octolytics-dimension-repository_network_root_id27193779
octolytics-dimension-repository_network_root_nwonodejs/node
turbo-body-classeslogged-out env-production page-responsive
disable-turbofalse
browser-stats-urlhttps://api.github.com/_private/browser/stats
browser-errors-urlhttps://api.github.com/_private/browser/errors
release98b41fa4b5635ceb45a4c54320fced73817ff290
ui-targetfull
theme-color#1e2327
color-schemelight dark
react-app-nameissues

Links:

Skip to contenthttps://github.com/nodejs/node/issues/66441#start-of-content
https://github.com/
Sign inhttps://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode%2Fissues%2F66441
GitHubRepository hosting, code review, and collaborationhttps://github.com/platform
CopilotAI coding agent for tasks, projects, and automated workhttps://github.com/features/copilot
ActionsCI/CD and automationhttps://github.com/features/actions
Code QualityQuality analysis, fixes, and enforcementhttps://github.com/features/code-quality
CodespacesCloud development environmentshttps://github.com/features/codespaces
Code SecurityVulnerability detection and remediationhttps://github.com/security/advanced-security/code-security
Secret ProtectionSecret scanning and leak preventionhttps://github.com/security/advanced-security/secret-protection
All Featureshttps://github.com/features
Documentationhttps://docs.github.com/en
Traininghttps://learn.github.com/skills
Certificationshttps://learn.github.com/credentials
Build with Copilothttps://github.com/features/ai
Customize Copilothttps://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-copilot-overview
Integrationshttps://github.com/marketplace
Trending reposhttps://github.com/trending
GitHub Sponsorshttps://github.com/open-source/sponsors
Developer articleshttps://github.com/resources/articles
ChangelogYour source for the latest features, improvements, and fixes across GitHub products, all in one place.https://github.blog/changelog/
Enterpriseshttps://github.com/enterprise
Teamhttps://github.com/team
Startupshttps://github.com/enterprise/startups
Nonprofitshttps://github.com/solutions/industry/nonprofits
Healthcarehttps://github.com/solutions/industry/healthcare
Financial serviceshttps://github.com/solutions/industry/financial-services
Manufacturinghttps://github.com/solutions/industry/manufacturing
Governmenthttps://github.com/solutions/industry/government
See all industrieshttps://github.com/solutions/industry
Why GitHubhttps://github.com/why-github
Customer Storieshttps://github.com/customer-stories
Business insightshttps://github.com/solutions/executive-insights
Trust Centerhttps://github.com/trust-center
GitHub Advanced SecurityEnterprise-grade code securityhttps://github.com/security/advanced-security
Copilot for businessEnterprise-grade AI featureshttps://github.com/features/copilot/copilot-business
Premium SupportEnterprise-grade 24/7 supporthttps://github.com/enterprise/premium-support
Documentationhttps://docs.github.com/en
Resource centerhttps://github.com/resources
Bloghttps://github.blog/
Events & webinarshttps://github.com/resources/events
Community Forumhttps://github.com/orgs/community/discussions
Customer Storieshttps://github.com/customer-stories
Integrationshttps://github.com/marketplace
Customer supporthttps://help.github.com/
Traininghttps://learn.github.com/skills
GitHub Statushttps://www.githubstatus.com/
Abouthttps://github.com/about
Partnershttps://github.com/partners
Changeloghttps://github.blog/changelog/
GitHub UniverseThe event for developers, by developers — talks, workshops, and what’s next for GitHub.https://githubuniverse.com/
Pricinghttps://github.com/pricing
Sign inhttps://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnodejs%2Fnode%2Fissues%2F66441
Sign uphttps://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fvoltron%2Fissues_fragments%2Fissue_layout&source=header-repo&source_repo=nodejs%2Fnode
Reloadhttps://github.com/nodejs/node/issues/66441
Reloadhttps://github.com/nodejs/node/issues/66441
Reloadhttps://github.com/nodejs/node/issues/66441
nodejshttps://github.com/nodejs
nodehttps://github.com/nodejs/node
Codehttps://github.com/nodejs/node
Issues656 (656)https://github.com/nodejs/node/issues
Pull requests666 (666)https://github.com/nodejs/node/pulls
Actionshttps://github.com/nodejs/node/actions
Projectshttps://github.com/nodejs/node/projects
Security and qualityhttps://github.com/nodejs/node/security
Insightshttps://github.com/nodejs/node/pulse
https://github.com/nodejs/node
New issuehttps://github.com/login?return_to=https://github.com/nodejs/node/issues/66441
New issuehttps://github.com/login?return_to=https://github.com/nodejs/node/issues/66441
#66470https://github.com/nodejs/node/pull/66470
node-api: objects from node_api_create_object_with_properties are in dictionary modehttps://github.com/nodejs/node/issues/66441#top
#66470https://github.com/nodejs/node/pull/66470
feature requestIssues requesting new Node.js features.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22feature%20request%22
node-apiIssues and PRs related to Node-API.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22node-api%22
https://github.com/colinhacks
colinhackshttps://github.com/colinhacks
on Oct 1, 2026https://github.com/nodejs/node/issues/66441#issue-5666961551
#59953https://github.com/nodejs/node/pull/59953
napi-rs/napi-rs#3559https://github.com/napi-rs/napi-rs/issues/3559
#59802https://github.com/nodejs/node/pull/59802
#60033https://github.com/nodejs/node/pull/60033
#61432https://github.com/nodejs/node/pull/61432
#66385https://github.com/nodejs/node/pull/66385
MikeMcC399https://github.com/MikeMcC399
feature requestIssues requesting new Node.js features.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22feature%20request%22
node-apiIssues and PRs related to Node-API.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22node-api%22
on Oct 1, 2026https://github.com/nodejs/node/issues/66441#event-32279099658
github-project-automationhttps://github.com/apps/github-project-automation
Node-API Team Projecthttps://github.com/orgs/nodejs/projects/11
Node.js feature requestshttps://github.com/orgs/nodejs/projects/4
on Oct 1, 2026https://github.com/nodejs/node/issues/66441#event-32406859539
github-project-automationhttps://github.com/apps/github-project-automation
Node.js feature requestshttps://github.com/orgs/nodejs/projects/4
on Oct 1, 2026https://github.com/nodejs/node/issues/66441
Sign up for freehttps://github.com/signup?return_to=https://github.com/nodejs/node/issues/66441
Sign in to commenthttps://github.com/login?return_to=https://github.com/nodejs/node/issues/66441
feature requestIssues requesting new Node.js features.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22feature%20request%22
node-apiIssues and PRs related to Node-API.https://github.com/nodejs/node/issues?q=state%3Aopen%20label%3A%22node-api%22
Node.js feature requestshttps://github.com/orgs/nodejs/projects/4
Node-API Team Projecthttps://github.com/orgs/nodejs/projects/11
https://github.com
Termshttps://docs.github.com/site-policy/github-terms/github-terms-of-service
Privacyhttps://docs.github.com/site-policy/privacy-policies/github-privacy-statement
Securityhttps://github.com/security
Statushttps://www.githubstatus.com/
Communityhttps://github.community/
Docshttps://docs.github.com/
Contacthttps://support.github.com?tags=dotcom-footer

Viewport: width=device-width


URLs of crawlers that visited me.