Skip to content

Collapse ElementState into a thin effect adapter - #3822

Closed
mattgperry wants to merge 1 commit into
perf/drop-motion-value-state-latestfrom
refactor/effect-subject
Closed

mattgperry wants to merge 1 commit into
perf/drop-motion-value-state-latestfrom
refactor/effect-subject

Conversation

@mattgperry

Copy link
Copy Markdown
Collaborator

Stacked on #3819 (which is stacked on #3815).

Summary

animate()'s element path had grown an ElementState class re-implementing getValue-with-default, addValue and read normalisation on top of styleEffect. This moves each concern to where it belongs, so what's left is a five-line binding of (effect, element) to the resolver's VisualElement-shaped interface, holding no state of its own.

  • Resolver no longer asks the element to get-or-create a value. DOMKeyframesResolver already holds this.motionValue, which is the value it was fetching via element.getValue(name, measureKeyframe). getValue(key) is now only a lookup of the other transform keys it zeroes for measurement, i.e. effect.get. The create-with-default overload is gone from the interface.
  • Origin normalisation lives in KeyframesResolver. Numeric strings → numbers and non-animatable "none" → animatable zero are applied once to whatever the raw read returns, instead of being duplicated in every readValue implementation. readValue on the element side is now just effect.read. (VisualElement.readValue keeps its copy for now because it also feeds setBaseTarget; the second pass is a no-op on already-normalised values.)
  • Effects expose flush(subject) so the resolver can write pending values before measuring. render() is effect.flush.
  • readStyleValue reports an unreadable style as 0, matching VisualElement.readValueFromInstance — this was previously hidden in the adapter.
  • One animation loop. animateElement and animateEffectSubject share animateValues(getValue, keyframes, transition, element?); they differ only in how a value is fetched or created (effect.get + bind vs visualElement.getValue(key, null)). The skip-when-already-at-target and transitionEnd handling now apply to effect subjects too.

What remains (EffectSubject) exists because the resolver and WAAPI still speak to an object shaped like a VisualElement, and VisualElement is the other implementor. It goes when the resolver can take (effect, subject) directly, after motion.* moves onto styleEffect.

Size (gzip, vs #3819)

bundle before after
animate 19.17 kB 19.11 kB
dom-animation 14.14 kB 14.13 kB
dom-max 27.38 kB 27.37 kB
style-effect 4.11 kB 4.12 kB

Test plan

  • yarn test — all packages green
  • Cypress on React 18 and 19: animate-unit-types, unit-conversion, animate-read-transform, animate-height-border-box, animate-filter-blur, waapi, waapi-svg, animate-style, animate-cancel, animate-reverse, waapi-interrupt-transform, animate-sequence-spring — all passing

Made with Cursor

animate()'s element path had grown an ElementState class that re-implemented
getValue-with-default, addValue and read normalisation on top of styleEffect.
Move each concern to where it belongs so the class becomes a five-line
binding of (effect, element) to the resolver's VisualElement-shaped interface:

- The DOM keyframe resolver uses its own motionValue for measurement instead
  of asking the element to get-or-create it; getValue(key) is now only a
  lookup of the other transform keys it zeroes, i.e. effect.get.
- Origin normalisation (numeric strings, animatable "none") happens in
  KeyframesResolver on whatever the raw read returns, rather than in each
  readValue implementation.
- Effects expose flush(subject) so the resolver can write pending values
  before measuring.
- readStyleValue reports an unreadable style as 0, as VisualElement does.
- animateElement and animateEffectSubject share one loop (animateValues),
  differing only in how a value is fetched or created.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mattgperry

Copy link
Copy Markdown
Collaborator Author

Folded into #3815.

@mattgperry mattgperry closed this Sep 14, 2026
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