Skip to content

ci: move v3 benchmarks into a browser, drop CodSpeed, and measure v3 against v4 - #830

Merged
titouanmathis merged 6 commits into
mainfrom
feature/benchmarks-v3-browser
Aug 16, 2026
Merged

ci: move v3 benchmarks into a browser, drop CodSpeed, and measure v3 against v4#830
titouanmathis merged 6 commits into
mainfrom
feature/benchmarks-v3-browser

Conversation

@titouanmathis

@titouanmathis titouanmathis commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

CodSpeed is gone, v3's benchmarks run in Chromium like v4's, and the mounting comparison between the two versions is now measured rather than assumed.

Why CodSpeed went

None of the last twelve merged pull requests changed a file under packages/js-toolkit/, yet CodSpeed failed on five of them and all five merged anyway. On #829 it read fail → fail → pass → fail → pass while that package's content never changed once. A check that fails on an unchanged package is not a signal, and a signal everyone has learned to ignore costs more than no check. Regressions are tracked by .github/actions/bench-diff instead: base and head measured on one runner, alternating, commented and never blocking.

Why the browser

v4's suite already runs in Chromium. A v3-against-v4 figure measured with v3 in happy-dom and v4 in a real browser compares two environments, not two frameworks. The browser is also the honest one on its own terms: v3's SmartQueue yields through setTimeout, whose clamping — and the longtask accounting that goes with it — exists only in a browser. resolve.conditions: ['typescript'] is kept, so the suite still measures src/.

How "done" is defined

The Nth mounted() call, then a main thread that has gone quiet. The count comes from the fixtures themselves, so it belongs to neither framework — using v4's whenDOMSettled() would have let v4 define away work it defers. The quiet check awaits a timer probe and a background-task probe together, because v3 queues on setTimeout and v4 posts background tasks, and a probe on one queue can overtake pending work on the other. Its idle cost measures at 0.1–0.7 ms, so it is not a meaningful tax on either side.

Each version is driven through its own native path — v3's document mutation observer feeding SmartQueue, v4's observer feeding its scheduler. Nothing normalises that difference away; it is the subject.

Every iteration swaps one page for another rather than appending to a growing one. v3 rescans the whole document on each mutation where v4 walks the inserted subtree, so a document that grew across a cycle would charge v3 for the benchmark's own bookkeeping. The cost is that a swap includes a destroy, which the destroy groups price separately.

The measurement

Local, Chromium, median of five swaps per cell. npm run bench:profile -w @studiometa/js-toolkit.

scenario size version settle (ms) quiet (ms) TBT (ms) longest task (ms) long tasks
control 100 v3 0.20 0.00 0.00 0
control 100 v4 0.70 0.00 0.00 0
flat 100 v3 7.00 7.30 0.00 0.00 0
flat 100 v4 2.80 3.50 0.00 0.00 0
nested 100 v3 7.50 8.20 0.00 0.00 0
nested 100 v4 2.60 2.70 0.00 0.00 0
realistic 100 v3 23.70 24.30 0.00 0.00 0
realistic 100 v4 14.00 15.60 0.00 0.00 0
control 1000 v3 0.90 0.00 0.00 0
control 1000 v4 2.80 0.00 0.00 0
flat 1000 v3 65.30 68.80 0.00 0.00 0
flat 1000 v4 21.30 21.40 0.00 0.00 0
nested 1000 v3 65.30 69.10 0.00 0.00 0
nested 1000 v4 21.10 21.20 0.00 0.00 0
realistic 1000 v3 202 217 0.00 0.00 0
realistic 1000 v4 126 126 0.00 0.00 0
control 5000 v3 20.60 0.00 0.00 0
control 5000 v4 26.50 0.00 0.00 0
flat 5000 v3 6635 6723 37.00 87.00 1
flat 5000 v4 108 108 0.00 0.00 0
nested 5000 v3 6356 6373 38.00 88.00 1
nested 5000 v4 115 115 0.00 0.00 0
realistic 5000 v3 6059 6249 71.00 121 1
realistic 5000 v4 501 501 29.00 79.00 1

control is a data-component name nothing is registered under, with the observer live: the discovery floor. It mounts nothing, so only its quiet time means anything.

v4's inView and responsive scenarios have no v3 side. Both are declared in v4's markup (data-mount="in-view", data-option-x:s) and v3 has neither attribute — its analogues are the withMountWhenInView and withResponsiveOptions decorators, which are a different unit of work wrapped around the component rather than a cost the registry pays per element. They stay in v4's own file.

Read as ratios

Same scenario, same size, same observable definition of done, so these are fair by construction.

scenario 100 1 000 5 000
flat v4 2.5× faster v4 3.1× v4 61×
nested v4 2.9× v4 3.1× v4 55×
realistic v4 1.7× v4 1.6× v4 12×
control (quiet) v3 3.5× v3 3.1× v3 1.3×

What the numbers say

v4 is faster and blocks less. There is no trade-off to report. The case the correction was written to catch — v4 slower end to end while blocking less, or the reverse — does not occur at any size or scenario. v4's total blocking time is zero everywhere v3's is zero, and lower everywhere v3's is not.

v3 wins the discovery floor. For a name nothing is registered under, v3 is 1.3–3.5× cheaper: it iterates its registry and never looks at an unknown name, while v4's observer walks the inserted subtree. That is a real cost v4 pays and v3 does not, and it is the one column where the rewrite went backwards. At 5 000 elements it is 27 ms against 21 ms — small against the 6-second gap in the row below it, but it is the honest counter-entry.

SmartQueue does not stop v3 blocking. At 5 000 components v3 produces a single 87–121 ms task on every scenario. The 40 ms budget only stops the queue starting another task past the limit; it cannot chunk a task that is itself long, and v3's registry scan — one queued closure that walks the document and constructs every instance — is exactly such a task. v4 produces no long task at all below the realistic 5 000 case.

v3 falls off a cliff between 1 000 and 5 000 components. 65 ms → 6 635 ms is 102× for 5× the work; v4 goes 21 ms → 108 ms, which is linear. The cause looks like SmartQueue.run(), which drains its task list with Array#shift. V8 abandons the fast path for large arrays, measured standalone on this machine:

queued tasks drained with shift drained by index
10 000 2.3 ms 0.1 ms
30 000 1 289 ms 0.2 ms
60 000 5 211 ms 0.4 ms
120 000 48 400 ms 0.3 ms

Each v3 $mount() queues about six closures, so 5 000 components put roughly 30 000 tasks in one queue — squarely in the collapse. This is a v3 bug, not a benchmark artefact, and it is a small fix (Queue.run taking tasks[i] instead of tasks.shift()). It is not fixed here: this pull request's job was to make the number visible, and changing v3's queue on the same branch would have measured the fix rather than the state of things. Worth its own issue.

On a cross-version guard: I recommend against a hard gate

A spec asserting "v4's total blocking time is no worse than v3's" would be poison, for three reasons.

  1. TBT is a cliff, not a curve. It counts only the excess above 50 ms, so a scenario whose tasks sit near the threshold reports 0 on a fast runner and tens of milliseconds on a slow one. Every cell above 1 000 components reports exactly 0.00. A threshold on a quantity that is zero until it suddenly is not gives no early warning and flakes at the boundary.
  2. The gate gets easier when v3 gets worse. v3 is a moving target with its own maintenance. A v3 regression would loosen a v3-relative assertion — a guard that rewards the other side degrading is not a guard.
  3. v4 already has the right gate, and it is absolute. packages/v4/src/mount-at-scale.spec.ts asserts longestTask === 0 for 500 realistic components and bounds the settle time, with thresholds that do not depend on runner speed and do not mention v3. That is where "v4 must not block" belongs, and it is where a future change would actually be caught.

So: keep the cross-version blocking numbers as a reported figure — mount-at-scale.profile.ts, refreshed on demand — and if v4's blocking behaviour needs defending harder, tighten the absolute budget in v4's own spec rather than tying it to v3. The one v3-relative check I would consider is a very loose sanity floor (v4's longest task never more than 2× v3's), and even that will need retiring the day v3 is dropped, so it buys little.

What CI samples

benchmarks-v3.yml, filtered to packages/js-toolkit/**, runs mount-at-scale.bench.ts only, at size 1 000, three rounds per side. Measured on this pull request: 1 min 33 s end to end, install and Chromium download included — in the same range as benchmarks-v4.yml.

  • The other ten benchmark files stay for local use (npm run bench). They were readable under instruction counting and are not readable on a wall clock: Chromium clamps performance.now() to 100 µs, so a microsecond body reports zero.
  • 5 000 is not sampled. One v3 swap there costs over five seconds, for the Array#shift reason above.
  • 100 is not sampled either; several of its cells sit under bench-diff's 5 ms resolution floor.
  • A separate workflow rather than a job in benchmarks-v4.yml, for the reason the bench-diff README gives: two suites in one job share a base checkout, and only a workflow-level paths: filter keeps each off the pull requests that do not concern it. The base of this pull request has no mount-at-scale.bench.ts, so the first run will report an empty base and say so — the action's documented behaviour for the pull request that adds a suite.

One thing found along the way

v3 and v4 cannot share a document. Both publish their instances on el.__base__. A live v4 registry walks a removed subtree, finds v3's entries there and calls $destroy() on them, which throws outright once v3 has left its 'terminated' string marker in that map. This is why the two versions take the document in turns here, v3 first — v4 installs its mutation processor on first registration and never removes it, while v3's registry is a plain map on globalThis that can be emptied. It also means a page cannot run both versions during a migration unless one of them is left unregistered. Worth knowing before anyone plans an incremental rollout.

The sticky comment on this pull request is the action's first run for this suite: the base has no mount-at-scale.bench.ts, so it reports an empty base and says so, which is its documented behaviour for the pull request that adds a suite. Its numbers also reproduce the ratios above on a different machine — v3 32.4 ms against v4 12.4 ms flat, 106.1 against 59.5 realistic, 24.4 against 3.2 destroy.

Verification

npm run lint, npm run lint:types, npm test (113 files, 982 passed), npm run test:v4 (79 files, 1 066 passed), and the full browser benchmark suite all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9

titouanmathis and others added 3 commits August 16, 2026 15:11
CodSpeed is removed because it did not work. None of the last twelve
merged pull requests changed a file under `packages/js-toolkit/`, yet it
failed on five of them and all five merged anyway. On #829 it read fail,
fail, pass, fail, pass while that package's content never changed once.
A check that fails on an unchanged package is not a signal, and a signal
everyone has learned to ignore costs more than no check.

happy-dom is removed because the numbers were not comparable to
anything. v4's suite runs in Chromium, and a v3-against-v4 figure means
nothing until both sides share an environment. The browser is also the
honest one on its own terms: v3's `SmartQueue` yields through
`setTimeout`, whose clamping — and the `longtask` accounting that goes
with it — exists only in a browser.

`resolve.conditions` is kept, so the suite still measures `src/` rather
than a stale `dist/`. The decorator transform is imported from v4 rather
than copied, because `v3-vs-v4.bench.ts` pulls in v4 sources and stage-3
decorators are not lowered by Vite's TypeScript transformer.

Files no longer run in parallel: they share the browser's one main
thread, and sampled together they measure each other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
…profile

Mirrors v4's `mount-at-scale.bench.ts` on the v3 side and measures both
versions in one browser session, so the rewrite's gains and losses are
numbers rather than assumptions.

Each version is driven through its own native path — v3's document
mutation observer feeding `SmartQueue`, v4's observer feeding its
scheduler. That difference is the subject, so nothing normalises it away.
What is identical is the definition of done, and it belongs to neither
framework: the Nth `mounted()` call, counted by the fixtures themselves,
followed by a main thread that has gone quiet. Using v4's
`whenDOMSettled()` would have let v4 define away work it defers.

Throughput alone cannot say whether v4 gave back what `SmartQueue` was
written to buy, so `mount-at-scale.profile.ts` reports the blocking
profile too: settle time, time to a quiet thread, total blocking time,
longest task and long-task count. It runs under `vitest run` because
tinybench reports one number per benchmark and this needs five.

**The two versions cannot share a document.** Both publish instances on
`el.__base__`, so a live v4 registry walks a removed subtree, finds v3's
entries and calls `$destroy()` on them — which throws outright once v3
has left its `'terminated'` marker there. They take the document in
turns, v3 first, because v4 installs its mutation processor on first
registration and never removes it while v3's registry is a plain map on
`globalThis`.

Every iteration swaps one page for another instead of appending to a
growing one: v3 rescans the whole document on each mutation where v4
walks the inserted subtree, so a document that grew across a cycle would
charge v3 for the benchmark's own bookkeeping.

CI tracks the at-scale file at one size. The other ten benchmark files
stay for local use: they were readable under instruction counting and
are not readable on a wall clock, where Chromium's 100 us clamp reports
a microsecond body as zero. 5 000 components is not sampled either —
v3's queue drains with `Array#shift`, which V8 turns quadratic past
~16 000 entries, so one v3 swap there costs over five seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code Review

Risk: Low — No concrete blocking or behavioural defects were found in the reviewed changes; the benchmark migration is safe to merge aside from the unreviewed lockfile.

Moves the v3 benchmark suite from CodSpeed and happy-dom into Chromium, adds a cross-version mount-at-scale benchmark and blocking profile, and configures CI to compare base and head measurements. The fixtures coordinate v3 and v4's native registration paths while using framework-neutral mount and quiet-thread completion criteria.


Review usage: 66,726 in (43,504 cached) / 1,495 out tokens — $0.0212 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit a62fcef.

Previous review runs

Previous run archived 2026-08-16T15:49:46Z

Code Review

Risk: Medium — issues that should be addressed before merge.

Replaces CodSpeed/happy-dom with Chromium benchmarks, adds cross-version mounting fixtures and a blocking profile, and updates the benchmark configuration and changelog. The v3 workflow tracks only selected paths, but its benchmark imports and measures sources from packages/v4/, which is not included in that trigger.

1 issue found:

  • issue.github/workflows/benchmarks-v3.yml:31 — Trigger benchmarks when v4 sources change

Review usage: 78,074 in (42,658 cached) / 1,419 out tokens — $0.0285 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit cf64cb4.

Previous run archived 2026-08-16T15:19:30Z

Code Review

Risk: Low — no blocking issues; safe to merge aside from nits.

Moves v3 benchmarks from CodSpeed and happy-dom into Chromium, adds cross-version mount-at-scale throughput and blocking profiles, and configures CI to compare base and head measurements. It also removes the old CodSpeed workflow and updates the changelog and benchmark dependencies.


Review usage: 77,493 in (42,431 cached) / 1,045 out tokens — $0.0272 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit f70abfd.

Previous run archived 2026-08-16T15:17:46Z

Code Review

Risk: Low — no blocking issues; safe to merge aside from nits.

Moves v3 benchmarks from CodSpeed and happy-dom to Chromium, adds the shared v3/v4 mount-at-scale suite, and introduces a blocking profile. The workflow tracks only the at-scale benchmark through the bench-diff action, while the profile reports settle, quiet, and long-task metrics locally.


Review usage: 57,334 in (19,247 cached) / 775 out tokens — $0.0271 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 38b1118.

@github-actions

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

✅ No export size changes.

Unchanged (388)

@studiometa/js-toolkit

Export Size (gzip) Diff
(barrel) 17.44 kB
AbstractService 598 B
Base 9.06 kB
ComponentLoader 2.31 kB
DEFAULT_DIAGNOSTIC_PREFIX 102 B
DragService 2.02 kB
IDLE_TIMEOUT 57 B
KeyService 935 B
LoadService 666 B
MutationService 849 B
PointerService 1.13 kB
RafService 1020 B
ResizeService 1.12 kB
ScrollService 1.36 kB
VISIBLE_ROOT_MARGIN 72 B
autoload 2.4 kB
closestComponent 419 B
composeManifests 119 B
createApp 996 B
defineFeatures 326 B
defineManifest 512 B
fromMetaGlob 228 B
fromWebpackContext 131 B
getClosestParent 197 B
getDirectChildren 202 B
getInstanceFromElement 125 B
getInstances 187 B
getScopedGroups 104 B
importOnInteraction 926 B
importOnMediaQuery 243 B
importWhenIdle 225 B
importWhenPrefersMotion 271 B
importWhenVisible 935 B
isDirectChild 218 B
logTree 551 B
queryComponent 594 B
queryComponentAll 601 B
readEagerTokens 201 B
registerComponent 305 B
registerComponents 356 B
registerManifest 2.87 kB
registerManifests 2.89 kB
useDrag 2.05 kB
useKey 943 B
useLoad 676 B
useMutation 876 B
usePointer 1.15 kB
useRaf 1 kB
useResize 1.13 kB
useScroll 1.36 kB
utils 10.05 kB
utils/Queue 269 B
utils/SmartQueue 440 B
utils/addClass 240 B
utils/addStyle 239 B
utils/animate 3.34 kB
utils/boundingRectToCircle 206 B
utils/cache 208 B
utils/camelCase 405 B
utils/clamp 98 B
utils/clamp01 114 B
utils/collideCircleCircle 129 B
utils/collideCircleRect 192 B
utils/collidePointCircle 128 B
utils/collidePointRect 122 B
utils/collideRectRect 128 B
utils/createEaseInOut 123 B
utils/createEaseOut 91 B
utils/createElement 635 B
utils/createLocalStorage 1.32 kB
utils/createLocalStorageProvider 296 B
utils/createMemoryStorageProvider 174 B
utils/createNoopProvider 128 B
utils/createRange 115 B
utils/createSessionStorage 1.32 kB
utils/createSessionStorageProvider 288 B
utils/createStorage 1.3 kB
utils/createUrlSearchParamsInHashProvider 461 B
utils/createUrlSearchParamsInHashStorage 1.35 kB
utils/createUrlSearchParamsProvider 429 B
utils/createUrlSearchParamsStorage 1.34 kB
utils/damp 106 B
utils/dashCase 404 B
utils/debounce 122 B
utils/domScheduler 310 B
utils/ease 519 B
utils/easeInCirc 285 B
utils/easeInCubic 287 B
utils/easeInExpo 286 B
utils/easeInOutCirc 288 B
utils/easeInOutCubic 289 B
utils/easeInOutExpo 288 B
utils/easeInOutQuad 288 B
utils/easeInOutQuart 289 B
utils/easeInOutQuint 289 B
utils/easeInOutSine 288 B
utils/easeInQuad 285 B
utils/easeInQuart 286 B
utils/easeInQuint 286 B
utils/easeInSine 285 B
utils/easeLinear 77 B
utils/easeOutCirc 286 B
utils/easeOutCubic 288 B
utils/easeOutExpo 286 B
utils/easeOutQuad 286 B
utils/easeOutQuart 286 B
utils/easeOutQuint 286 B
utils/easeOutSine 286 B
utils/endsWith 128 B
utils/fold 168 B
utils/getAncestorWhere 123 B
utils/getAncestorWhereUntil 148 B
utils/getComponentResolver 140 B
utils/getOffsetSizes 194 B
utils/hasWindow 88 B
utils/historyPush 524 B
utils/historyReplace 526 B
utils/inertiaFinalValue 169 B
utils/isArray 63 B
utils/isBoolean 78 B
utils/isDefined 75 B
utils/isDev 78 B
utils/isEmpty 206 B
utils/isEmptyString 108 B
utils/isFunction 79 B
utils/isNull 68 B
utils/isNumber 91 B
utils/isObject 108 B
utils/isString 77 B
utils/keyCodes 122 B
utils/lerp 84 B
utils/loadElement 220 B
utils/loadIframe 241 B
utils/loadImage 241 B
utils/loadLink 237 B
utils/loadScript 251 B
utils/localStorageProvider 839 B
utils/lowerCase 404 B
utils/map 93 B
utils/matrix 136 B
utils/mean 126 B
utils/memo 130 B
utils/memoize 228 B
utils/memoryStorageProvider 843 B
utils/nextFrame 179 B
utils/nextMicrotask 133 B
utils/nextTick 148 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 322 B
utils/pascalCase 407 B
utils/random 93 B
utils/randomInt 113 B
utils/randomItem 234 B
utils/removeClass 242 B
utils/removeStyle 243 B
utils/round 95 B
utils/saveActiveElement 92 B
utils/scrollTo 2.31 kB
utils/sessionStorageProvider 838 B
utils/smoothTo 476 B
utils/snakeCase 406 B
utils/spring 154 B
utils/startsWith 125 B
utils/throttle 125 B
utils/toggleClass 242 B
utils/transform 347 B
utils/transition 1010 B
utils/trapFocus 441 B
utils/tween 1.72 kB
utils/untrapFocus 120 B
utils/upperCase 404 B
utils/urlSearchParamsInHashProvider 845 B
utils/urlSearchParamsProvider 839 B
utils/useScheduler 309 B
utils/wait 103 B
utils/withLeadingCharacters 135 B
utils/withLeadingSlash 142 B
utils/withTrailingCharacters 135 B
utils/withTrailingSlash 142 B
utils/withoutLeadingCharacters 122 B
utils/withoutLeadingCharactersRecursive 165 B
utils/withoutLeadingSlash 133 B
utils/withoutTrailingCharacters 122 B
utils/withoutTrailingCharactersRecursive 165 B
utils/withoutTrailingSlash 133 B
utils/wrap 122 B
version 56 B
withBreakpointManager 1.54 kB
withBreakpointObserver 1.71 kB
withDrag 2.18 kB
withExtraConfig 163 B
withFreezedOptions 187 B
withGroup 455 B
withIntersectionObserver 303 B
withMountOnMediaQuery 393 B
withMountWhenInView 347 B
withMountWhenPrefersMotion 431 B
withMutation 1010 B
withName 109 B
withRelativePointer 1.29 kB
withResponsiveOptions 2.4 kB
withScrolledInView 3.05 kB

@studiometa/js-toolkit-v4

Export Size (gzip) Diff
(barrel) 20.51 kB
BREAKPOINTS 776 B
Base 8.09 kB
DIAGNOSTICS 629 B
DRAG_MODES 162 B
EVENTS 155 B
MOUNT_ATTRIBUTE 69 B
SWAP_MODES 129 B
children 243 B
component 10.55 kB
createContext 472 B
createFallbackProvider 1.34 kB
createGroup 1.06 kB
createLocalStorage 2.34 kB
createLocalStorageProvider 1.23 kB
createMemoryStorageProvider 1.23 kB
createService 630 B
createServiceMixin 509 B
createSessionStorage 2.34 kB
createSessionStorageProvider 1.23 kB
createStorage 2.32 kB
createUrlSearchParamsInHashProvider 1.23 kB
createUrlSearchParamsInHashStorage 2.36 kB
createUrlSearchParamsProvider 1.23 kB
createUrlSearchParamsStorage 2.36 kB
defaultScheduler 1.5 kB
defineManifest 983 B
domUpdate 1.23 kB
emitExtendable 1.09 kB
fromMetaGlob 203 B
fromWebpackContext 131 B
getBreakpoints 776 B
getInstances 2.77 kB
inject 175 B
injectContext 675 B
injectContextSync 634 B
jsonSerializer 95 B
localStorageProvider 1.22 kB
memoryStorageProvider 1.23 kB
nextFrame 115 B
on 8.42 kB
perTarget 176 B
provide 178 B
provideContext 704 B
provideRootContext 748 B
read 127 B
registerComponent 10.47 kB
registerComponents 10.48 kB
registerManifest 10.56 kB
sessionStorageProvider 1.22 kB
setBreakpoints 805 B
signal 920 B
subscribeContext 1.45 kB
swap 2.63 kB
toggle 176 B
until 172 B
urlSearchParamsInHashProvider 1.22 kB
urlSearchParamsProvider 1.22 kB
useBreakpoint 1.44 kB
useDrag 3.22 kB
useInView 1.31 kB
useMediaQuery 1.05 kB
useMutation 1.29 kB
usePointer 1.72 kB
usePrefersReducedMotion 1.08 kB
useRaf 1.93 kB
useResize 1.3 kB
useScroll 2.54 kB
useScrollProgress 3.51 kB
useWindowScroll 2.52 kB
useWindowSize 1.3 kB
utils 8.65 kB
utils/DEFAULT_DAMP_FACTOR 109 B
utils/INERTIA_FRAME 97 B
utils/MAX_SPRING_RATIO 100 B
utils/SCROLL_AXES 100 B
utils/TRANSFORM_PROPS 137 B
utils/TRANSITION_OPTIONS 132 B
utils/camelCase 449 B
utils/capitalize 119 B
utils/clamp 133 B
utils/clamp01 149 B
utils/clampDampFactor 157 B
utils/createEaseInOut 120 B
utils/createEaseOut 91 B
utils/createElement 638 B
utils/createRange 205 B
utils/damp 211 B
utils/debounce 121 B
utils/decayOver 162 B
utils/deepmerge 312 B
utils/easeInCirc 94 B
utils/easeInCubic 81 B
utils/easeInExpo 97 B
utils/easeInOutCirc 150 B
utils/easeInOutCubic 141 B
utils/easeInOutExpo 150 B
utils/easeInOutQuad 139 B
utils/easeInOutQuart 140 B
utils/easeInOutQuint 140 B
utils/easeInOutSine 156 B
utils/easeInQuad 80 B
utils/easeInQuart 81 B
utils/easeInQuint 81 B
utils/easeInSine 104 B
utils/easeLinear 77 B
utils/easeOutCirc 121 B
utils/easeOutCubic 111 B
utils/easeOutExpo 124 B
utils/easeOutQuad 110 B
utils/easeOutQuart 112 B
utils/easeOutQuint 111 B
utils/easeOutSine 132 B
utils/enterTransition 679 B
utils/fold 200 B
utils/getOffsetSizes 268 B
utils/historyPush 380 B
utils/historyReplace 381 B
utils/inertiaDecay 199 B
utils/inertiaFinalValue 187 B
utils/inertiaStep 232 B
utils/inertiaTimeConstant 178 B
utils/isBoolean 90 B
utils/isDefined 87 B
utils/isFunction 86 B
utils/isNull 78 B
utils/isNumber 103 B
utils/isObject 115 B
utils/isString 89 B
utils/kebabCase 421 B
utils/leaveTransition 679 B
utils/lerp 120 B
utils/loadImage 245 B
utils/loadLink 776 B
utils/loadScript 697 B
utils/lowerCase 84 B
utils/map 128 B
utils/matrix 150 B
utils/mean 147 B
utils/memo 217 B
utils/noop 62 B
utils/noopValue 76 B
utils/objectToURLSearchParams 253 B
utils/pascalCase 434 B
utils/random 93 B
utils/randomInt 132 B
utils/randomItem 163 B
utils/round 130 B
utils/saveActiveElement 571 B
utils/scrollTo 1.72 kB
utils/selectorFor 2.72 kB
utils/setClassesOrStyles 218 B
utils/smoothTo 2.56 kB
utils/snakeCase 421 B
utils/spring 343 B
utils/throttle 151 B
utils/transform 286 B
utils/transition 577 B
utils/trapFocus 717 B
utils/untrapFocus 587 B
utils/upperCase 84 B
utils/wait 103 B
utils/withLeadingCharacters 142 B
utils/withLeadingSlash 152 B
utils/withTrailingCharacters 143 B
utils/withTrailingSlash 153 B
utils/withoutLeadingCharacters 127 B
utils/withoutLeadingCharactersRecursive 144 B
utils/withoutLeadingSlash 138 B
utils/withoutTrailingCharacters 129 B
utils/withoutTrailingCharactersRecursive 147 B
utils/withoutTrailingSlash 140 B
utils/wrap 154 B
viewTransition 1.66 kB
watchAttributes 1.98 kB
whenDOMSettled 2.31 kB
withDrag 3.61 kB
withInView 1.76 kB
withMutation 1.78 kB
withPointer 2.11 kB
withRaf 2.31 kB
withResize 1.7 kB
withScroll 2.93 kB
withScrollProgress 3.94 kB
write 125 B

The comments predicted a 4-5 ms tail from a clamped `setTimeout(0)`. The
measured idle round trip is 0.1-0.7 ms, so the threshold's justification
and the caveat about understating the faster version were both wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.16%. Comparing base (58ca803) to head (a62fcef).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #830   +/-   ##
=======================================
  Coverage   97.16%   97.16%           
=======================================
  Files         170      170           
  Lines        4133     4133           
  Branches     1151     1152    +1     
=======================================
  Hits         4016     4016           
  Misses        106      106           
  Partials       11       11           
Flag Coverage Δ
eslint-plugin-js-toolkit 93.79% <ø> (ø)
js-toolkit 97.92% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

v3 mount benchmarks

Base and head measured on this runner, alternating over 3 rounds each; every value is the median of the round medians. Running both sides on one machine is what removes cross-machine noise — a cached baseline from another runner would put it back.

A move under 25%, or on a benchmark under 5 ms, is not reported as a change: it is inside the measured noise of a shared runner.

The base commit has no benchmark suite, so every benchmark below is new. A base that failed to build or measure would have failed this job rather than appearing here.

Group Benchmark Base Head us / component Change
destroy 1000 flat components — v3 v3 — destroy - 27.8 ms 27.80 new
destroy 1000 flat components — v4 v4 — destroy - 2.90 ms 2.90 new
swap 1000 components — v3 v3 — control - 0.20 ms 0.20 new
swap 1000 components — v3 v3 — flat - 40.3 ms 40.30 new
swap 1000 components — v3 v3 — nested - 49.1 ms 49.10 new
swap 1000 components — v3 v3 — realistic - 126.8 ms 126.80 new
swap 1000 components — v4 v4 — control - 2.10 ms 2.10 new
swap 1000 components — v4 v4 — flat - 14.9 ms 14.90 new
swap 1000 components — v4 v4 — nested - 15.5 ms 15.50 new
swap 1000 components — v4 v4 — realistic - 77.1 ms 77.10 new

v4 declares both in markup; v3's analogues are decorators wrapped around
the component. Comparing them would compare a decorator against an
attribute, which is a different unit of work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

v4 mount benchmarks

Base and head measured on this runner, alternating over 3 rounds each; every value is the median of the round medians. Running both sides on one machine is what removes cross-machine noise — a cached baseline from another runner would put it back.

A move under 25%, or on a benchmark under 5 ms, is not reported as a change: it is inside the measured noise of a shared runner.

No benchmark moved beyond the noise floor.

Within noise (18)
Group Benchmark Base Head us / component Change
destroy 1000 flat components, one removal flat 2.40 ms 2.80 ms 2.80 +16.7%
destroy 5000 flat components, one removal flat 14.1 ms 16.9 ms 3.38 +19.9%
mount 1000 components, one insertion control — declared but unregistered 1.90 ms 1.90 ms 1.90 0.0%
mount 1000 components, one insertion flat 15.2 ms 15.3 ms 15.30 +0.7%
mount 1000 components, one insertion in-view — one controller per element 28.1 ms 30.7 ms 30.70 +9.3%
mount 1000 components, one insertion nested 4 deep 12.7 ms 12.7 ms 12.70 0.0%
mount 1000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 70.7 ms 68.8 ms 68.80 -2.7%
mount 1000 components, one insertion responsive option — breakpoint cascade per mount 14.5 ms 14.5 ms 14.50 0.0%
mount 1000 flat components, 1 vs 10 insertions 1 insertion 10.7 ms 10.7 ms 10.70 -0.0%
mount 1000 flat components, 1 vs 10 insertions 10 insertions 12.0 ms 11.8 ms 11.80 -1.7%
mount 5000 components, one insertion control — declared but unregistered 18.2 ms 17.4 ms 3.48 -4.4%
mount 5000 components, one insertion flat 59.0 ms 59.3 ms 11.86 +0.5%
mount 5000 components, one insertion in-view — one controller per element 143.8 ms 146.9 ms 29.38 +2.2%
mount 5000 components, one insertion nested 4 deep 61.0 ms 62.0 ms 12.40 +1.6%
mount 5000 components, one insertion realistic — 5 refs, 3 options, 4 handlers 332.0 ms 331.0 ms 66.20 -0.3%
mount 5000 components, one insertion responsive option — breakpoint cascade per mount 79.1 ms 79.9 ms 15.98 +1.0%
mount 5000 flat components, 1 vs 10 insertions 1 insertion 61.7 ms 62.6 ms 12.52 +1.5%
mount 5000 flat components, 1 vs 10 insertions 10 insertions 61.8 ms 62.4 ms 12.48 +1.0%

Comment thread .github/workflows/benchmarks-v3.yml
The cross-version suite imports `packages/v4/src/index.js` and reuses
v4's SWC decorator plugin, so a v4-only pull request moves half of what
it measures — and the path filter would have skipped it on exactly the
changes most likely to regress the comparison.

Found by @weareikko/code-review on #830.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNdFD3aQhzfdm3EsCSbS9
@titouanmathis
titouanmathis merged commit ca52f96 into main Aug 16, 2026
12 checks passed
@titouanmathis
titouanmathis deleted the feature/benchmarks-v3-browser branch August 16, 2026 15:52
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.

1 participant