Skip to content

Tree-shake feature-specific compat hooks - #5203

Merged
JoviDeCroock merged 1 commit into
mainfrom
agent/tree-shake-compat-feature-hooks
Aug 13, 2026
Merged

Tree-shake feature-specific compat hooks#5203
JoviDeCroock merged 1 commit into
mainfrom
agent/tree-shake-compat-feature-hooks

Conversation

@JoviDeCroock

@JoviDeCroock JoviDeCroock commented Aug 11, 2026

Copy link
Copy Markdown
Member

Note

The idea was created by Jovi and this PR was authored by GPT5.6-sol

Resolves #1529

Description

We need tree-shaking in preact/compat. A lot of the options hooks are exclusive to a given feature. In this PR we target use/uSES/Suspense.

Measured from the production compat/dist/compat.mjs generated by npm run build, then tree-shaken with Rollup's defaults, minified with Terser, and compressed with gzip:

  • useState: 2146 B → 1551 B (-595 B / 27.7%)
  • forwardRef: 2248 B → 1650 B (-598 B / 26.6%)
  • memo: 2262 B → 1674 B (-588 B / 26.0%)
  • useSyncExternalStore: 2341 B → 1785 B (-556 B / 23.8%)
  • use: 2280 B → 1734 B (-546 B / 23.9%)
  • Suspense: 2154 B → 2159 B (+5 B / 0.2%)
  • lazy: 2241 B → 1649 B (-592 B / 26.4%)

The production build preserves the /* @__PURE__ */ initializers. These results use Rollup's default propertyReadSideEffects: true; setting it to false produced identical output.

The compressed-size check measures the complete compat artifact, where every export is present, so it instead reports +62 B gzip. The build reports +67 B Brotli, and a downstream consumer retaining the default export grows by 38 B.

Notes

In an ideal world we could also remove the options.event, options.vnode, and textarea options.diffed hooks from the base cost of compat. That would let people select individual React compatibility features without incurring the cost of those hooks.

A production-build experiment gating all three behind pure render/hydrate initializers produced these representative gzip results compared with this PR:

  • useState: 1551 B → 369 B (-1182 B)
  • forwardRef: 1650 B → 496 B (-1154 B)
  • Suspense: 2159 B → 1026 B (-1133 B)
  • render: 1609 B → 1628 B (+19 B)
  • default import: unchanged
  • complete compat artifact: 3916 B → 3930 B (+14 B)

This would be a breaking change. If an application does not retain render or hydrate from compat, isValidElement(createElement('div')) can return false. It can also skip $$typeof, defaultProps, class-component ref handling, DOM prop normalization, React event normalization, and the textarea fix when compat VNodes are consumed by another or separately bundled renderer.

Installing the hooks inside the runtime render() call is too late because the root VNode, and often its children, have already been created. If we want this, we should chase it separately.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: unsure 🔍 -1% - +1% (-6.72ms - +6.53ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -1% - +0% (-0.15ms - +0.04ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -2% - +1% (-0.90ms - +0.58ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -2% - +2% (-0.30ms - +0.26ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -1% - +2% (-0.69ms - +0.84ms)
    preact-local vs preact-main
  • text-update: faster ✔ 0% - 9% (0.00ms - 0.19ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -1% - +4% (-0.35ms - +1.03ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -4% - +1% (-1.34ms - +0.27ms)
    preact-local vs preact-main

usedJSHeapSize

  • create10k: unsure 🔍 -0% - +0% (-0.03ms - +0.01ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -4% - +4% (-0.20ms - +0.19ms)
    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.00ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -5% - +2% (-0.05ms - +0.02ms)
    preact-local vs preact-main
  • todo: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -0% - +0% (-0.01ms - +0.00ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local682.16ms - 693.63ms-unsure 🔍
-1% - +1%
-6.72ms - +6.53ms
preact-main684.67ms - 691.31msunsure 🔍
-1% - +1%
-6.53ms - +6.72ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local19.23ms - 19.26ms-unsure 🔍
-0% - +0%
-0.03ms - +0.01ms
preact-main19.24ms - 19.27msunsure 🔍
-0% - +0%
-0.01ms - +0.03ms
-
filter-list

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.44ms - 16.62ms-unsure 🔍
-1% - +0%
-0.15ms - +0.04ms
preact-main16.56ms - 16.61msunsure 🔍
-0% - +1%
-0.04ms - +0.15ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.57ms - 1.58ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main1.57ms - 1.58msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-
hydrate1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local54.13ms - 55.12ms-unsure 🔍
-2% - +1%
-0.90ms - +0.58ms
preact-main54.23ms - 55.33msunsure 🔍
-1% - +2%
-0.58ms - +0.90ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local4.97ms - 5.28ms-unsure 🔍
-4% - +4%
-0.20ms - +0.19ms
preact-main5.01ms - 5.25msunsure 🔍
-4% - +4%
-0.19ms - +0.20ms
-
many-updates

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.44ms - 16.84ms-unsure 🔍
-2% - +2%
-0.30ms - +0.26ms
preact-main16.47ms - 16.85msunsure 🔍
-2% - +2%
-0.26ms - +0.30ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.76ms - 3.76ms-unsure 🔍
-0% - +0%
-0.00ms - +0.01ms
preact-main3.76ms - 3.76msunsure 🔍
-0% - +0%
-0.01ms - +0.00ms
-
replace1k
  • Browser: chrome-headless
  • Sample size: 100
  • Built by: CI #5827
  • Commit: 0ed7f4c

duration

VersionAvg timevs preact-localvs preact-main
preact-local54.05ms - 55.15ms-unsure 🔍
-1% - +2%
-0.69ms - +0.84ms
preact-main54.00ms - 55.06msunsure 🔍
-2% - +1%
-0.84ms - +0.69ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.03ms - 3.03ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main3.03ms - 3.03msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local26.37ms - 27.13ms-unsure 🔍
-3% - +1%
-0.89ms - +0.34ms
preact-main26.54ms - 27.51msunsure 🔍
-1% - +3%
-0.34ms - +0.89ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local31.38ms - 32.73ms-unsure 🔍
-2% - +4%
-0.59ms - +1.24ms
preact-main31.11ms - 32.34msunsure 🔍
-4% - +2%
-1.24ms - +0.59ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local31.11ms - 32.72ms-unsure 🔍
-3% - +4%
-1.10ms - +1.17ms
preact-main31.07ms - 32.69msunsure 🔍
-4% - +3%
-1.17ms - +1.10ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local25.29ms - 25.59ms-unsure 🔍
-0% - +1%
-0.09ms - +0.30ms
preact-main25.21ms - 25.46msunsure 🔍
-1% - +0%
-0.30ms - +0.09ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local26.55ms - 27.56ms-unsure 🔍
-2% - +3%
-0.67ms - +0.72ms
preact-main26.56ms - 27.50msunsure 🔍
-3% - +2%
-0.72ms - +0.67ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local20.52ms - 20.91ms-unsure 🔍
-1% - +2%
-0.20ms - +0.33ms
preact-main20.47ms - 20.83msunsure 🔍
-2% - +1%
-0.33ms - +0.20ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 220
  • Built by: CI #5827
  • Commit: 0ed7f4c

duration

VersionAvg timevs preact-localvs preact-main
preact-local1.87ms - 1.99ms-faster ✔
0% - 9%
0.00ms - 0.19ms
preact-main1.96ms - 2.10msslower ❌
0% - 10%
0.00ms - 0.19ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.04ms - 1.09ms-unsure 🔍
-5% - +2%
-0.05ms - +0.02ms
preact-main1.06ms - 1.11msunsure 🔍
-2% - +5%
-0.02ms - +0.05ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local22.90ms - 24.26ms-unsure 🔍
-1% - +4%
-0.35ms - +1.03ms
preact-main23.13ms - 23.33msunsure 🔍
-4% - +1%
-1.03ms - +0.35ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.28ms - 1.28ms-unsure 🔍
+0% - +0%
+0.00ms - +0.00ms
preact-main1.28ms - 1.28msunsure 🔍
-0% - -0%
-0.00ms - -0.00ms
-
update10th1k
  • Browser: chrome-headless
  • Sample size: 120
  • Built by: CI #5827
  • Commit: 0ed7f4c

duration

VersionAvg timevs preact-localvs preact-main
preact-local31.38ms - 32.44ms-unsure 🔍
-4% - +1%
-1.34ms - +0.27ms
preact-main31.83ms - 33.05msunsure 🔍
-1% - +4%
-0.27ms - +1.34ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.98ms - 2.99ms-unsure 🔍
-0% - +0%
-0.01ms - +0.00ms
preact-main2.98ms - 2.99msunsure 🔍
-0% - +0%
-0.00ms - +0.01ms
-

tachometer-reporter-action v2 for CI

@github-actions

Copy link
Copy Markdown

Size Change: +62 B (+0.39%)

Total Size: 15.9 kB

📦 View Changed
Filename Size Change
compat/dist/compat.mjs 3.95 kB +62 B (+1.6%)
ℹ️ View Unchanged
Filename Size
debug/dist/debug.mjs 3.88 kB
devtools/dist/devtools.mjs 272 B
dist/preact.mjs 4.88 kB
hooks/dist/hooks.mjs 1.56 kB
jsx-runtime/dist/jsxRuntime.mjs 856 B
test-utils/dist/testUtils.mjs 473 B

compressed-size-action

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 98.61% (-0.7%) from 99.297% — agent/tree-shake-compat-feature-hooks into main

@JoviDeCroock
JoviDeCroock marked this pull request as ready for review August 13, 2026 07:59
@JoviDeCroock
JoviDeCroock merged commit 86944f4 into main Aug 13, 2026
12 of 13 checks passed
@JoviDeCroock
JoviDeCroock deleted the agent/tree-shake-compat-feature-hooks branch August 13, 2026 16:02
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.

Maybe provide tree-shakeable alternative to preact/compat?

3 participants