Skip to content

Flush pending effects before a queued rerender - #5296

Draft
JoviDeCroock wants to merge 4 commits into
mainfrom
JoviDeCroock/flush-effects-before-rerender
Draft

JoviDeCroock wants to merge 4 commits into
mainfrom
JoviDeCroock/flush-effects-before-rerender

Conversation

@JoviDeCroock

@JoviDeCroock JoviDeCroock commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Fixes #3666

A rerender that is queued before the previous commit's useEffects have run now runs them first, as React does. Radix registers Slider thumbs in an effect and reads them in the rerender that a ref callback queues, so before this change the thumbs never got an index and stayed hidden.

Builds on #5265 (its two commits are included here): an effect error now throws out of the flush before anything renders, and the queue is rescheduled instead of dropped, so neither the effect error nor a later render error is lost.

The flush happens in core, through a private options._flushEffects hook that process() calls before draining the queue. #5163 tried to get the same behaviour by wrapping options.debounceRendering from preact/hooks, which had these problems:

  • Any later assignment to that public option silently dropped the flush, so behaviour depended on import order and on whoever set a scheduler last.
  • setupRerender()/act replaced the wrapper, so tests ran with different effect timing than apps, and test-utils needed patches.
  • +39 B brotli on hooks alone.

Here process() runs on every path, including custom schedulers and act. Only effects of finished commits that kept their tree are flushed early:

  • A commit's effects become flushable after its layout effects have run, so a synchronous rerender during a commit (flushSync in a ref or layout effect, a sync debounceRendering) still runs layout effects first.
  • After a render that caught an error or suspended, effects wait for the frame as before, because the boundary's rerender discards part of that tree. Siblings of a thrower under an error boundary or a suspending lazy() don't mount and unmount their effects.
  • Pending effects are taken before they run, so an effect that throws, or calls flushSync, doesn't run twice.

Cost: core +6 B (including #5265) and hooks +36 B brotli.

Verified with @radix-ui/react-slider@1.4.5: on 11.0.0 both thumbs sit at 0% with no aria-valuenow; with this branch they render at 25% and 75% and keyboard changes move them.

Note

Verified by GPT6.1-sol

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: unsure 🔍 -3% - +0% (-16.11ms - +2.81ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 -0% - +0% (-0.03ms - +0.03ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -3% - +0% (-1.74ms - +0.18ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -0% - +0% (-0.07ms - +0.06ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -1% - +1% (-0.59ms - +0.77ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -8% - +6% (-0.09ms - +0.06ms)
    preact-local vs preact-main
  • todo: unsure 🔍 -1% - +1% (-0.18ms - +0.31ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -5% - +3% (-1.46ms - +0.81ms)
    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.16ms - +0.51ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -3% - +4% (-0.03ms - +0.04ms)
    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.01ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local541.77ms - 552.04ms-unsure 🔍
-3% - +0%
-16.11ms - +2.81ms
preact-main545.61ms - 561.49msunsure 🔍
-1% - +3%
-2.81ms - +16.11ms
-

usedJSHeapSize

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

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.53ms - 16.58ms-unsure 🔍
-0% - +0%
-0.03ms - +0.03ms
preact-main16.53ms - 16.57msunsure 🔍
-0% - +0%
-0.03ms - +0.03ms
-

usedJSHeapSize

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

duration

VersionAvg timevs preact-localvs preact-main
preact-local55.81ms - 57.11ms-unsure 🔍
-3% - +0%
-1.74ms - +0.18ms
preact-main56.53ms - 57.94msunsure 🔍
-0% - +3%
-0.18ms - +1.74ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local5.74ms - 6.29ms-unsure 🔍
-3% - +9%
-0.16ms - +0.51ms
preact-main5.65ms - 6.03msunsure 🔍
-8% - +3%
-0.51ms - +0.16ms
-
many-updates

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.49ms - 16.60ms-unsure 🔍
-0% - +0%
-0.07ms - +0.06ms
preact-main16.52ms - 16.58msunsure 🔍
-0% - +0%
-0.06ms - +0.07ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.75ms - 3.75ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
preact-main3.74ms - 3.75msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-
replace1k
  • Browser: chrome-headless
  • Sample size: 100
  • Built by: CI #6065
  • Commit: 0bfde8d

duration

VersionAvg timevs preact-localvs preact-main
preact-local80.62ms - 81.76ms-unsure 🔍
-1% - +1%
-0.59ms - +0.77ms
preact-main80.73ms - 81.46msunsure 🔍
-1% - +1%
-0.77ms - +0.59ms
-

usedJSHeapSize

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

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local25.68ms - 26.47ms-unsure 🔍
-4% - +0%
-1.12ms - +0.10ms
preact-main26.12ms - 27.05msunsure 🔍
-0% - +4%
-0.10ms - +1.12ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local29.95ms - 31.07ms-unsure 🔍
-5% - +0%
-1.55ms - +0.10ms
preact-main30.63ms - 31.84msunsure 🔍
-0% - +5%
-0.10ms - +1.55ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local30.64ms - 31.96ms-unsure 🔍
-3% - +3%
-1.05ms - +0.91ms
preact-main30.65ms - 32.09msunsure 🔍
-3% - +3%
-0.91ms - +1.05ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local27.58ms - 28.34ms-unsure 🔍
-1% - +3%
-0.32ms - +0.70ms
preact-main27.44ms - 28.11msunsure 🔍
-2% - +1%
-0.70ms - +0.32ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local19.35ms - 19.53ms-unsure 🔍
-1% - +1%
-0.12ms - +0.22ms
preact-main19.25ms - 19.53msunsure 🔍
-1% - +1%
-0.22ms - +0.12ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local24.69ms - 25.17ms-unsure 🔍
-0% - +2%
-0.01ms - +0.50ms
preact-main24.60ms - 24.76msunsure 🔍
-2% - +0%
-0.50ms - +0.01ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 250
  • Built by: CI #6065
  • Commit: 0bfde8d

duration

VersionAvg timevs preact-localvs preact-main
preact-local1.00ms - 1.10ms-unsure 🔍
-8% - +6%
-0.09ms - +0.06ms
preact-main1.01ms - 1.12msunsure 🔍
-6% - +8%
-0.06ms - +0.09ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.07ms - 1.12ms-unsure 🔍
-3% - +4%
-0.03ms - +0.04ms
preact-main1.06ms - 1.11msunsure 🔍
-4% - +3%
-0.04ms - +0.03ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local31.77ms - 32.13ms-unsure 🔍
-1% - +1%
-0.18ms - +0.31ms
preact-main31.72ms - 32.05msunsure 🔍
-1% - +1%
-0.31ms - +0.18ms
-

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

duration

VersionAvg timevs preact-localvs preact-main
preact-local30.96ms - 32.44ms-unsure 🔍
-5% - +3%
-1.46ms - +0.81ms
preact-main31.16ms - 32.88msunsure 🔍
-3% - +5%
-0.81ms - +1.46ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.97ms - 2.98ms-unsure 🔍
-0% - +0%
-0.01ms - +0.01ms
preact-main2.97ms - 2.98msunsure 🔍
-0% - +0%
-0.01ms - +0.01ms
-

tachometer-reporter-action v2 for CI

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Size Change: +59 B (+0.37%)

Total Size: 16 kB

📦 View Changed
Filename Size Change
dist/preact.mjs 4.94 kB +11 B (+0.22%)
hooks/dist/hooks.mjs 1.63 kB +49 B (+3.09%)
test-utils/dist/testUtils.mjs 472 B -1 B (-0.21%)
ℹ️ View Unchanged
Filename Size
compat/dist/compat.mjs 3.95 kB
debug/dist/debug.mjs 3.9 kB
devtools/dist/devtools.mjs 268 B
jsx-runtime/dist/jsxRuntime.mjs 868 B

compressed-size-action

@coveralls

coveralls commented Oct 4, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 99.853% (+0.001%) from 99.852% — JoviDeCroock/flush-effects-before-rerender into main

A render that throws now leaves the rest of the queue scheduled, so
teardown() can render a second throwing component. Restore
debounceRendering in a finally and catch the error in act(), keeping
the first one, so actDepth and requestAnimationFrame are always reset.

This branch has not been deployed

No deployments
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.

[preact/compat] radix-ui Slider component drag points (Thumbs) not working

2 participants