Skip to content

feat: implement getServerSnapshot for useSyncExternalStore#4979

Open
ssssota wants to merge 10 commits into
preactjs:mainfrom
ssssota:getServerSnapshot
Open

feat: implement getServerSnapshot for useSyncExternalStore#4979
ssssota wants to merge 10 commits into
preactjs:mainfrom
ssssota:getServerSnapshot

Conversation

@ssssota

@ssssota ssssota commented Dec 20, 2025

Copy link
Copy Markdown
Contributor

close #4972

@github-actions

github-actions Bot commented Dec 20, 2025

Copy link
Copy Markdown

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: unsure 🔍 -0% - +1% (-3.07ms - +5.48ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +0% (-0.05ms - +0.05ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -1% - +2% (-0.56ms - +1.17ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -0% - +4% (-0.07ms - +0.64ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -2% - +2% (-1.38ms - +1.33ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -1% - +6% (-0.02ms - +0.12ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -1% - +1% (-0.19ms - +0.20ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -5% - +5% (-1.37ms - +1.36ms)
    preact-local vs preact-main

usedJSHeapSize

  • create10k: unsure 🔍 +0% - +0% (+0.01ms - +0.01ms)
    preact-local vs preact-main
  • filter-list: slower ❌ 1% - 1% (0.01ms - 0.02ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -4% - +4% (-0.22ms - +0.19ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 +0% - +0% (+0.01ms - +0.01ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 +0% - +0% (+0.01ms - +0.01ms)
    preact-local vs preact-main
  • text-update: slower ❌ 1% - 6% (0.01ms - 0.06ms)
    preact-local vs preact-main
  • todo: slower ❌ 1% - 1% (0.01ms - 0.01ms)
    preact-local vs preact-main
  • update10th1k: slower ❌ 0% - 1% (0.00ms - 0.02ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local831.75ms - 839.10ms-unsure 🔍
-0% - +1%
-3.07ms - +5.48ms
preact-main832.04ms - 836.40msunsure 🔍
-1% - +0%
-5.48ms - +3.07ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local19.09ms - 19.09ms-unsure 🔍
+0% - +0%
+0.01ms - +0.01ms
preact-main19.08ms - 19.08msunsure 🔍
-0% - -0%
-0.01ms - -0.01ms
-
filter-list

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.51ms - 16.57ms-unsure 🔍
-0% - +0%
-0.05ms - +0.05ms
preact-main16.49ms - 16.58msunsure 🔍
-0% - +0%
-0.05ms - +0.05ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.57ms - 1.57ms-slower ❌
1% - 1%
0.01ms - 0.02ms
preact-main1.55ms - 1.56msfaster ✔
1% - 1%
0.01ms - 0.02ms
-
hydrate1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local56.58ms - 57.67ms-unsure 🔍
-1% - +2%
-0.56ms - +1.17ms
preact-main56.15ms - 57.49msunsure 🔍
-2% - +1%
-1.17ms - +0.56ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local5.00ms - 5.28ms-unsure 🔍
-4% - +4%
-0.22ms - +0.19ms
preact-main5.00ms - 5.30msunsure 🔍
-4% - +4%
-0.19ms - +0.22ms
-
many-updates

duration

VersionAvg timevs preact-localvs preact-main
preact-local15.97ms - 16.47ms-unsure 🔍
-0% - +4%
-0.07ms - +0.64ms
preact-main15.68ms - 16.18msunsure 🔍
-4% - +0%
-0.64ms - +0.07ms
-

usedJSHeapSize

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

duration

VersionAvg timevs preact-localvs preact-main
preact-local57.34ms - 59.19ms-unsure 🔍
-2% - +2%
-1.38ms - +1.33ms
preact-main57.30ms - 59.29msunsure 🔍
-2% - +2%
-1.33ms - +1.38ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.02ms - 3.03ms-unsure 🔍
+0% - +0%
+0.01ms - +0.01ms
preact-main3.01ms - 3.01msunsure 🔍
-0% - -0%
-0.01ms - -0.01ms
-

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local28.96ms - 29.71ms-unsure 🔍
-2% - +2%
-0.50ms - +0.56ms
preact-main28.93ms - 29.68msunsure 🔍
-2% - +2%
-0.56ms - +0.50ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local34.76ms - 36.01ms-unsure 🔍
-1% - +4%
-0.38ms - +1.32ms
preact-main34.34ms - 35.49msunsure 🔍
-4% - +1%
-1.32ms - +0.38ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local30.11ms - 31.24ms-unsure 🔍
-2% - +3%
-0.57ms - +0.96ms
preact-main29.97ms - 31.00msunsure 🔍
-3% - +2%
-0.96ms - +0.57ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local26.64ms - 27.07ms-unsure 🔍
-2% - +2%
-0.57ms - +0.42ms
preact-main26.48ms - 27.37msunsure 🔍
-2% - +2%
-0.42ms - +0.57ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local24.51ms - 26.01ms-unsure 🔍
-5% - +3%
-1.31ms - +0.81ms
preact-main24.77ms - 26.26msunsure 🔍
-3% - +5%
-0.81ms - +1.31ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local21.22ms - 21.65ms-unsure 🔍
-2% - +1%
-0.43ms - +0.24ms
preact-main21.28ms - 21.78msunsure 🔍
-1% - +2%
-0.24ms - +0.43ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 220
  • Built by: CI #5706
  • Commit: 70af09a

duration

VersionAvg timevs preact-localvs preact-main
preact-local1.97ms - 2.07ms-unsure 🔍
-1% - +6%
-0.02ms - +0.12ms
preact-main1.92ms - 2.02msunsure 🔍
-6% - +1%
-0.12ms - +0.02ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.03ms - 1.08ms-slower ❌
1% - 6%
0.01ms - 0.06ms
preact-main1.01ms - 1.03msfaster ✔
1% - 6%
0.01ms - 0.06ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local31.79ms - 32.07ms-unsure 🔍
-1% - +1%
-0.19ms - +0.20ms
preact-main31.78ms - 32.06msunsure 🔍
-1% - +1%
-0.20ms - +0.19ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.28ms - 1.28ms-slower ❌
1% - 1%
0.01ms - 0.01ms
preact-main1.27ms - 1.27msfaster ✔
1% - 1%
0.01ms - 0.01ms
-
update10th1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local28.14ms - 30.15ms-unsure 🔍
-5% - +5%
-1.37ms - +1.36ms
preact-main28.23ms - 30.07msunsure 🔍
-5% - +5%
-1.36ms - +1.37ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.98ms - 3.00ms-slower ❌
0% - 1%
0.00ms - 0.02ms
preact-main2.97ms - 2.99msfaster ✔
0% - 1%
0.00ms - 0.02ms
-

tachometer-reporter-action v2 for CI

@coveralls

coveralls commented Dec 20, 2025

Copy link
Copy Markdown

Coverage Status

coverage: 98.339% (-0.2%) from 98.541% — ssssota:getServerSnapshot into preactjs:main

Comment thread compat/src/hooks.js Outdated
Comment thread compat/test/browser/useSyncExternalStore.test.jsx
@ssssota
ssssota marked this pull request as ready for review December 20, 2025 02:00
Comment thread compat/src/hooks.js Outdated
Comment thread compat/src/hooks.js Outdated
Comment thread compat/src/hooks.js Outdated
ssssota and others added 2 commits December 20, 2025 23:33
Co-Authored-By: rschristian <rschristian@users.noreply.github.com>
Co-Authored-By: rschristian <rschristian@users.noreply.github.com>
@ssssota
ssssota requested a review from rschristian December 20, 2025 14:42
Comment thread compat/src/hooks.js Outdated
getServerSnapshot
) {
const value =
typeof window === 'undefined' || hydrating

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This or we use options._skipEffects

Comment thread compat/src/hooks.js Outdated
typeof window === 'undefined' || hydrating
? getServerSnapshot
? getServerSnapshot()
: missingGetServerSnapshot()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add oversized error strings, this seems a bit against our bundle philosophy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That (the oversized string) was my suggestion FWIW, my bad

Comment thread compat/src/hooks.js Outdated
let oldBeforeRender = options._render;

/** @type {(vnode: import('./internal').VNode) => void} */
options._render = _vnode => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move this to the render.js file you can benefit from options._render there and save more bytes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having hooks.js only contain some hooks, not all, is kinda weird... what was the byte difference there, if you remember? Should we even keep this module around, or should it be inlined into render?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried it locally it was 32b gzip

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roughly a third then, hefty.

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general trend of this looks good, left some comments on how to reduce bundle size. Let's get this into v11

@ssssota
ssssota requested a review from JoviDeCroock July 18, 2026 23:20
@ssssota

ssssota commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

@JoviDeCroock Thank you for the review! I've made the changes, so please take a look.

@JoviDeCroock

Copy link
Copy Markdown
Member

I meant move the whole hook over 😅 this made the byte size regress

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add tests in the suspense-hydration case whether this works correctly. When a resumed boundary continues hydration we expect components there to correctly follow the server snapshot

Comment thread compat/src/render.js Outdated
) {
const serverRendering = options._skipEffects || hydrationRoot;
if (serverRendering && !getServerSnapshot) {
throw new Error('Missing getServerSnapshot');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the error all together imho, we can just fall back to regular snapshot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make that change as suggested!
Please note that React throws an error in this case.

If you omit this argument, rendering the component on the server will throw an error.

useSyncExternalStore – React

@ssssota
ssssota requested a review from JoviDeCroock July 20, 2026 06:30
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.

Support for the third argument of useSyncExternalStore

4 participants