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
Domain: github.com
{"@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-profiling | 0 |
| react-import-map | react,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-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:072f3d00-7929-3cbb-eb36-e32dbd0e3c91 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | D24A:3C4A99:655140:83A44C:6AED2BE3 |
| html-safe-nonce | b349b115e8d5684357ddfa6c646275ef185349624c5c3cd1d94496b75d68447f |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEMjRBOjNDNEE5OTo2NTUxNDA6ODNBNDRDOjZBRUQyQkUzIiwidmlzaXRvcl9pZCI6IjI0MzA1ODc3NzYzMTk0MzM2OTkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | cd701f41f7fb2a8394e2bacfa8b23c7a59844a0ff93144f6157859574240f85a |
| hovercard-subject-tag | issue:5666961551 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/66441/issue_layout |
| twitter:image | https://opengraph.githubassets.com/4d6a8cc3808837e10a86a25aaa2fa7f62d322baebb5e7d68afc05966c163acea/nodejs/node/issues/66441 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/4d6a8cc3808837e10a86a25aaa2fa7f62d322baebb5e7d68afc05966c163acea/nodejs/node/issues/66441 |
| og:image:alt | 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... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | colinhacks |
| hostname | github.com |
| expected-hostname | github.com |
| None | 9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 98b41fa4b5635ceb45a4c54320fced73817ff290 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width