# \`Object.upgrade()\`: Like \`Object.setPrototypeOf()\` but only for prototype specialization at the leafs

**URL:** <https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618>\
**Category:** 💡 Ideas\
**Created:** [July 3, 2026, 7:51pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618 "2026-07-03T19:51:21Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![LeaVerou](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/leaverou/32/1660_2.png) [@LeaVerou](https://es.discourse.group/u/LeaVerou)\
**Post date:** [July 3, 2026, 7:51pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/1 "2026-07-03T19:51:21Z")

</div>

## Why?

We have `Object.setPrototypeOf()` but it's so slow in the general case that the messaging is basically "never use this".

However, literally every single use case I've ever encountered for it was much more narrowly scoped, not just a general "change an object's prototype to whatever". Namely, it satisfied these two invariants:

1. all were around **specializing** an existing prototype to a different prototype that inherits from it, not changing it to a completely different shape
2. all were around upgrading **leafs** , i.e. objects that aren't generally used as prototypes themselves in the codebase.

This is also how a custom element upgrade works: first the HTML parser creates an `HTMLElement` instance when encountering an unknown hyphenated element, then once a definition is registered current and future **instances** get "upgraded" to a subclass of `HTMLElement` . Except, instead of referencing an ES concept, this is defined somewhat ad hoc in the HTML spec. A first-class language concept could even allow [generalizing this concept](https://github.com/whatwg/html/issues/11914), opening up a lot of nice use cases around lazily upgrading instances as needed (more broadly, not just around DOM use cases).

My hypothesis is that a separate function that preserves these invariants (and throws if they are not followed):

- More clearly communicates intent. You no longer need to reach for a machete when a nail file would do. E.g. I can imagine style guides allowing `Object.upgrade()` but forbidding `Object.setPrototypeOf()`.
- Prevents unintentional footguns.
- Could potentially be implemented more efficiently. AFAIK a lot of the cost of `Object.setPrototypeOf()` is around the ripple effect of mutating an object that is _itself_ a prototype of other objects, and the prototype chain being unstable. `Object.upgrade()` guarantees that objects are only ever extended at the very end, their shape only ever extended rather than mutated — no different than adding a bunch of properties.

## How?

For the first invariant (specializing, not replacing), the behavior would go a bit like this (illustrative; assuming no methods are shadowed):

```javascript
Object.upgrade(obj, proto) {
  const cur = Object.getPrototypeOf(obj);
  if (cur === proto) return obj; // no-op

  if (cur !== null && !cur.isPrototypeOf(proto)) {
    // not an extension of the current chain
    throw new TypeError(/* ... */);
  }

  return Object.setPrototypeOf(obj, proto);
}

```

The second invariant (no ripple effects) is trickier. How do we guarantee that the object being upgraded is a leaf, i.e. not a prototype of anything else?

1. ❌ Restrict it to certain types of objects
  - But any object can be a prototype of another, either manually or via `Object.create()` ! Reject.

2. ❌ Restrict it to objects that are not _currently_ prototypes (GC dependent)
  - Probably too unstable, might require additional tracking by engines, exposes GC, and is not justified by use cases. Reject.

3. Restrict it to objects that have **never been prototypes**
  - New [[HasBeenPrototype]] internal property
  - At least [V8 already tracks this](https://github.com/v8/v8/blob/5fe0aa3bc79c0a9d3ad546b79211f07105f09585/src/objects/map.h#L160)
  - Problem: the committee has previously felt strongly against notifying objects that they were extended and throwing exposes this, but perhaps this is more agreeable given the much more limited surface and the cost-benefit?

For that to work, it's important that this operates on the internal [[Prototype]] and is not affected by `Symbol.hasInstance` and friends, otherwise we're back in the land of Anything Goes.

Thoughts? I’d especially love to hear from implementors whether this may actually be more efficient. If folks think it may be worth it, I can turn it into a proposal pretty quickly.

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [July 4, 2026, 3:12am UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/2 "2026-07-04T03:12:29Z")

</div>

I'm not sure I understand the motivation. If the problem is an optimization question, why can't the answer be an optimization of the engines for these use cases? Aka if there are common use cases for updating an object's prototype, can't the engines avoid deopt in those cases?

---

<div class="post-metadata">

**Author:** ![conartist6](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/conartist6/32/515_2.png) [@conartist6](https://es.discourse.group/u/conartist6)\
**Post date:** [July 4, 2026, 1:51pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/3 "2026-07-04T13:51:14Z")

</div>

The main problem is that it'll bust a bunch of inline caches -- anything that had ever seen the un-upgraded object. That's a double whammy if not a triple whammy. You paid a cost to create optimized code, then you pay a cost deoptimize the code, then you pay a cost to reoptimize the formerly-monomorphic code as polymorphic code, and finally you pay the cost of operating permanently in the slower polymorphic regime (or megamorphic regime even perhaps).

My impression then is that `Object.upgrade()` would be no more and no less of a potential perf footgun than `Object.setPrototypeOf()` already is. Sometimes it would be fine, and sometimes it would cause massive slowdown. Only a few people would have the depth of knowledge to understand how to get the good result

---

<div class="post-metadata">

**Author:** ![conartist6](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/conartist6/32/515_2.png) [@conartist6](https://es.discourse.group/u/conartist6)\
**Post date:** [July 4, 2026, 2:05pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/4 "2026-07-04T14:05:07Z")

</div>

Two of the main sources of difficulty:

1. ICs are allll over the place and they need to be very memory-compact.
2. Even the fast monomorphic property access (which compiles to pointer arithmetic) needs to be able to validate _each time it is used_ that the conditions for the optimization remain valid.

Therein lies the trickiness: for memory offset access to be a good optimization, the check to make sure the optimization is still valid cannot be significantly slower than the operation itself. You can't pair a slow prototype-chain-walking deopt check with a super fast single-memory-access operation. Even if the slow check would allow you to realize that the optimization was still valid, the fact that you have to do the slow check every time would eliminate all the perf benefits of the fast implementation

Right now engines implement the check for a monomorphic IC as a single numeric comparison: is the address of the root node of the property transition chain exactly the same as it was? This is very fast/efficient, but leaves no room to handle subtyping in any other way than deoptimization.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [July 7, 2026, 9:15am UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/5 "2026-07-07T09:15:45Z")

</div>

Not sure this is needed or if it helps but I don't find current _upgrade_ particularly slow, or better, specially with DOM elements is not slower than just creating nodes, which is an inevitable payload.

```javascript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script type="module">
    import custom from 'https://esm.run/custom-function/factory';

    class LI extends custom(HTMLLIElement) {
      get text() {
        return this.textContent;
      }
    }

    const many = 1000;
    const items = [];

    console.time('create');
    for (let li, i = 0; i < many; i++) {
      li = document.createElement('li');
      li.textContent = `Item ${i}`;
      items.push(li);
    }
    console.timeEnd('create');

    list.append(...items);

    console.time('upgrade');
    for (let li, i = 0; i < many; i++)
      new LI(items[i]);
    console.timeEnd('upgrade');

    console.log(items[0].text);
  </script>
</head>
<body>
  <ul id="list"></ul>
</body>
</html>

```

You could comment out the `textContent = ....` and see that upgrade costs almost like create but it's true that upgrading feels unnecessarily slow, with or without the reference being known or leaked around already, see this variant with creation time almost doubled despite the node being instantly upgraded:

```javascript
import custom from 'https://esm.run/custom-function/factory';

class LI extends custom(HTMLLIElement) {
  get text() {
    return this.textContent;
  }
}

const many = 1000;
const items = [];

console.time('create');
for (let li, i = 0; i < many; i++) {
  li = new LI(document.createElement('li'));
  li.textContent = `Item ${i}`;
  items.push(li);
}
console.timeEnd('create');

list.append(...items);

console.log(items[0].text);

```

---

<div class="post-metadata">

**Author:** ![LeaVerou](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/leaverou/32/1660_2.png) [@LeaVerou](https://es.discourse.group/u/LeaVerou)\
**Post date:** [July 7, 2026, 2:07pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/6 "2026-07-07T14:07:38Z")

</div>

@WebReflection

AFAIK the DOM is slow enough that it dwarfs any language-level feature. Proxies are the same: while they are considered slow for many pure JS use cases, they are totally fine for most DOM use cases because the relative difference is meaningless compared to how long DOM operations take.

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [July 7, 2026, 2:39pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/7 "2026-07-07T14:39:10Z")

</div>

true and true but, to be clear, no Proxy in here, just the setPrototypeOf update for that DOM node. I am not sure that DOM node is to blame for slowness but the custom utility is nothing more than this [custom-function/esm/factory.js at 6d72d2a294f7090fa0ed5a21d3c7b8b464c92ffd · WebReflection/custom-function · GitHub](https://github.com/WebReflection/custom-function/blob/6d72d2a294f7090fa0ed5a21d3c7b8b464c92ffd/esm/factory.js)

It could work with literally any other native JS API but back to raw operation, it's kinda slow indeed ...

```javascript
const { setPrototypeOf } = Object;

class Put extends Map {
  put(key, value) {
    super.set(key, value);
    return value;
  }
}

const many = 1000;
const items = [];

console.time('create');
for (let i = 0; i < many; i++) {
  items.push(new Map);
}
console.timeEnd('create');

console.time('upgrade');
for (let i = 0; i < many; i++)
  setPrototypeOf(items[i], Put.prototype);
console.timeEnd('upgrade');

console.log(items[0].put('key', 'value'));

```

On average that's slower than just creating a new instance but I guess this kind of micro-benchmark might not reveal all the details ... but then again, it sums up even on direct creation without leaking further any reference around:

```javascript
const { setPrototypeOf } = Object;

class Put extends Map {
  put(key, value) {
    super.set(key, value);
    return value;
  }
}

const many = 1000;
const items = [];

console.time('create');
for (let i = 0; i < many; i++) {
  items.push(setPrototypeOf(new Map, Put.prototype));
}
console.timeEnd('create');

console.log(items[0].put('key', 'value'));

```

compared to just `new Put` that's like 1.5x slower

---

<div class="post-metadata">

**Author:** ![WebReflection](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/webreflection/32/248_2.png) [@WebReflection](https://es.discourse.group/u/WebReflection)\
**Post date:** [July 9, 2026, 8:20pm UTC](https://es.discourse.group/t/object-upgrade-like-object-setprototypeof-but-only-for-prototype-specialization-at-the-leafs/2618/8 "2026-07-09T20:20:56Z")

</div>

a thing I've been thinking about is the `Object.getOwnPropertyDescriptors(Class.prototype)` used as `Object.defineProperties(ref, that_result)` which should at least grant semantics and _maybe_ not be too slower on the process ... as an upgrade, that looks like a better variant, isn't it?
