Skip to content

fix: vnode clone compatible with frozen intrinsics#5078

Open
kumavis wants to merge 1 commit intopreactjs:mainfrom
kumavis:fix-hardenedjs-compat
Open

fix: vnode clone compatible with frozen intrinsics#5078
kumavis wants to merge 1 commit intopreactjs:mainfrom
kumavis:fix-hardenedjs-compat

Conversation

@kumavis
Copy link
Copy Markdown

@kumavis kumavis commented Apr 25, 2026

This PR fixes a compatibility issue when Preact is run in Hardened JavaScript environments such as:

The compatibility issue comes from a javascript quirk known as "the override mistake" related to frozen intrinsics. For this bug, the minimal environment change is just Object.freeze(Object.prototype).

In this environment, assigning the constructor property via assignment syntax (=) or Object.assign causes an error.

  TypeError: Cannot assign to read only property 'constructor'

Specifically, Object.assign({}, vnode) in renderComponent() (and cloneNode in src/diff/index.js) tries to copy a vnode's constructor: undefined own property via [[Set]]. Under environments with a frozen Object.prototype the Object.prototype.constructor is non-writable, so the assignment fails.

This PR introduces a cloneVNode utility that ensures VNodes are cloned safely. It also reorders the constructor key for consistent object shape between VNodes and their clones.

Note: I erred on the side of excessive documentation in comments, let me know if you would like this reduced.

Object.assign({}, vnode) in renderComponent() (and cloneNode in
src/diff/index.js) tries to copy a vnode's `constructor: undefined`
own property via [[Set]]. Under https://hardenedjs.org/ environment the default
overrideTaming makes Object.prototype.constructor non-writable, so
the assignment fails with:

  TypeError: Cannot assign to read only property 'constructor'

Initial render works because createVNode builds vnodes with an
object literal ([[CreateDataProperty]], not Set), but any
setState/forceUpdate-triggered re-render goes through
Object.assign({}, oldVNode) and trips the override mistake.
@kumavis kumavis changed the title fix: vnode clone compatible with hardenedjs env fix: vnode clone compatible with frozen intrinsics Apr 25, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 25, 2026

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: unsure 🔍 -1% - +1% (-4.53ms - +5.38ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +1% (-0.07ms - +0.14ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -1% - +3% (-0.62ms - +1.81ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -1% - +3% (-0.13ms - +0.44ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -0% - +3% (-0.20ms - +1.56ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -5% - +4% (-0.10ms - +0.08ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -0% - +1% (-0.04ms - +0.30ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -2% - +5% (-0.51ms - +1.43ms)
    preact-local vs preact-main

usedJSHeapSize

  • create10k: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -3% - +9% (-0.18ms - +0.54ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 +0% - +0% (+0.00ms - +0.01ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 +0% - +0% (+0.00ms - +0.01ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -4% - +2% (-0.04ms - +0.02ms)
    preact-local vs preact-main
  • todo: unsure 🔍 +0% - +0% (+0.00ms - +0.01ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -0% - +1% (-0.00ms - +0.02ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local870.64ms - 877.69ms-unsure 🔍
-1% - +1%
-4.53ms - +5.38ms
preact-main870.26ms - 877.23msunsure 🔍
-1% - +1%
-5.38ms - +4.53ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local19.05ms - 19.05ms-unsure 🔍
+0% - +0%
+0.00ms - +0.00ms
preact-main19.04ms - 19.05msunsure 🔍
-0% - -0%
-0.00ms - -0.00ms
-
filter-list

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.49ms - 16.69ms-unsure 🔍
-0% - +1%
-0.07ms - +0.14ms
preact-main16.51ms - 16.59msunsure 🔍
-1% - +0%
-0.14ms - +0.07ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.55ms - 1.55ms-unsure 🔍
+0% - +0%
+0.00ms - +0.00ms
preact-main1.54ms - 1.54msunsure 🔍
-0% - -0%
-0.00ms - -0.00ms
-
hydrate1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local65.84ms - 67.74ms-unsure 🔍
-1% - +3%
-0.62ms - +1.81ms
preact-main65.44ms - 66.95msunsure 🔍
-3% - +1%
-1.81ms - +0.62ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local5.74ms - 6.28ms-unsure 🔍
-3% - +9%
-0.18ms - +0.54ms
preact-main5.59ms - 6.06msunsure 🔍
-9% - +3%
-0.54ms - +0.18ms
-
many-updates

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.36ms - 16.87ms-unsure 🔍
-1% - +3%
-0.13ms - +0.44ms
preact-main16.34ms - 16.59msunsure 🔍
-3% - +1%
-0.44ms - +0.13ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.73ms - 3.74ms-unsure 🔍
+0% - +0%
+0.00ms - +0.01ms
preact-main3.73ms - 3.73msunsure 🔍
-0% - -0%
-0.01ms - -0.00ms
-
replace1k
  • Browser: chrome-headless
  • Sample size: 100
  • Built by: CI #5480
  • Commit: daa9ab7

duration

VersionAvg timevs preact-localvs preact-main
preact-local58.58ms - 59.88ms-unsure 🔍
-0% - +3%
-0.20ms - +1.56ms
preact-main57.96ms - 59.14msunsure 🔍
-3% - +0%
-1.56ms - +0.20ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.00ms - 3.01ms-unsure 🔍
+0% - +0%
+0.00ms - +0.01ms
preact-main3.00ms - 3.00msunsure 🔍
-0% - -0%
-0.01ms - -0.00ms
-

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local27.77ms - 28.57ms-unsure 🔍
-1% - +4%
-0.16ms - +1.02ms
preact-main27.31ms - 28.17msunsure 🔍
-4% - +1%
-1.02ms - +0.16ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local33.32ms - 34.52ms-unsure 🔍
-2% - +3%
-0.53ms - +1.08ms
preact-main33.10ms - 34.18msunsure 🔍
-3% - +2%
-1.08ms - +0.53ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local32.83ms - 33.65ms-unsure 🔍
-3% - +0%
-1.05ms - +0.13ms
preact-main33.27ms - 34.12msunsure 🔍
-0% - +3%
-0.13ms - +1.05ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local25.77ms - 26.33ms-unsure 🔍
-1% - +1%
-0.28ms - +0.34ms
preact-main25.88ms - 26.16msunsure 🔍
-1% - +1%
-0.34ms - +0.28ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local24.09ms - 25.73ms-unsure 🔍
-5% - +4%
-1.22ms - +1.07ms
preact-main24.19ms - 25.79msunsure 🔍
-4% - +5%
-1.07ms - +1.22ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local21.17ms - 21.58ms-unsure 🔍
-1% - +2%
-0.19ms - +0.35ms
preact-main21.11ms - 21.47msunsure 🔍
-2% - +1%
-0.35ms - +0.19ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 170
  • Built by: CI #5480
  • Commit: daa9ab7

duration

VersionAvg timevs preact-localvs preact-main
preact-local2.00ms - 2.14ms-unsure 🔍
-5% - +4%
-0.10ms - +0.08ms
preact-main2.03ms - 2.14msunsure 🔍
-4% - +5%
-0.08ms - +0.10ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local0.99ms - 1.03ms-unsure 🔍
-4% - +2%
-0.04ms - +0.02ms
preact-main1.00ms - 1.04msunsure 🔍
-2% - +4%
-0.02ms - +0.04ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local30.03ms - 30.26ms-unsure 🔍
-0% - +1%
-0.04ms - +0.30ms
preact-main29.90ms - 30.14msunsure 🔍
-1% - +0%
-0.30ms - +0.04ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.26ms - 1.26ms-unsure 🔍
+0% - +0%
+0.00ms - +0.01ms
preact-main1.26ms - 1.26msunsure 🔍
-0% - -0%
-0.01ms - -0.00ms
-
update10th1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local30.99ms - 32.34ms-unsure 🔍
-2% - +5%
-0.51ms - +1.43ms
preact-main30.51ms - 31.91msunsure 🔍
-4% - +2%
-1.43ms - +0.51ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.96ms - 2.98ms-unsure 🔍
-0% - +1%
-0.00ms - +0.02ms
preact-main2.96ms - 2.97msunsure 🔍
-1% - +0%
-0.02ms - +0.00ms
-

tachometer-reporter-action v2 for CI

@kumavis kumavis marked this pull request as ready for review April 26, 2026 03:30
@coveralls
Copy link
Copy Markdown

Coverage Status

coverage: 98.666%. remained the same — kumavis:fix-hardenedjs-compat into preactjs:main

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants