-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Memoize projection style writes to skip redundant CSSOM setters #3793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -991,6 +991,7 @@ export function createProjectionNode<I>({ | |
| ) { | ||
| resetTransform(this.instance, transformTemplateValue) | ||
| this.shouldResetTransform = false | ||
| this.clearRenderCache() | ||
| this.scheduleRender() | ||
| } | ||
| } | ||
|
|
@@ -1153,6 +1154,13 @@ export function createProjectionNode<I>({ | |
| crossfade: | ||
| options.crossfade !== undefined ? options.crossfade : true, | ||
| } | ||
|
|
||
| /** | ||
| * A React render may have written styles outside of the | ||
| * projection pipeline, so the memoized projection writes can | ||
| * no longer be trusted. | ||
| */ | ||
| this.clearRenderCache() | ||
| } | ||
|
|
||
| clearMeasurements() { | ||
|
|
@@ -1490,6 +1498,7 @@ export function createProjectionNode<I>({ | |
| */ | ||
| if (this.prevProjectionDelta) { | ||
| this.createProjectionDeltas() | ||
| this.clearRenderCache() | ||
| this.scheduleRender() | ||
| } | ||
|
|
||
|
|
@@ -1553,10 +1562,17 @@ export function createProjectionNode<I>({ | |
| isVisible = true | ||
| hide() { | ||
| this.isVisible = false | ||
| /** | ||
| * While hidden, style renders write non-projected styles to | ||
| * the element, so the memoized projection writes can't be | ||
| * trusted once visibility changes. | ||
| */ | ||
| this.clearRenderCache() | ||
| // TODO: Schedule render | ||
| } | ||
| show() { | ||
| this.isVisible = true | ||
| this.clearRenderCache() | ||
| // TODO: Schedule render | ||
| } | ||
|
|
||
|
|
@@ -1984,23 +2000,70 @@ export function createProjectionNode<I>({ | |
| visualElement.scheduleRender() | ||
| } | ||
|
|
||
| /** | ||
| * Caches of the last projection-written transform styles. Layout | ||
| * animations render every projecting node every frame, but these | ||
| * writes often don't change frame-to-frame. Comparing against | ||
| * these caches lets us skip the CSSOM setter calls, which | ||
| * dominate per-frame projection cost at scale. | ||
| */ | ||
| renderedTransform: string | undefined | ||
| renderedOriginX: number = -1 | ||
| renderedOriginY: number = -1 | ||
| wroteHidden = false | ||
|
|
||
| clearRenderCache() { | ||
| this.renderedTransform = undefined | ||
| this.renderedOriginX = this.renderedOriginY = -1 | ||
| } | ||
|
|
||
| /** | ||
| * Whether applyProjectionStyles will write a transform this | ||
| * render. When true, style renders can skip writing user | ||
| * transforms as projection owns (and incorporates) them, | ||
| * avoiding a doubled CSSOM write per projecting element per | ||
| * frame and keeping the memoized projection writes valid. | ||
| */ | ||
| willProjectTransform() { | ||
| if (!this.instance || this.isSVG || !this.isVisible) { | ||
| return false | ||
| } | ||
|
|
||
| if (this.needsReset) return true | ||
|
|
||
| return Boolean( | ||
| this.projectionDelta && this.layout && this.getLead().target | ||
| ) | ||
| } | ||
|
|
||
| applyProjectionStyles( | ||
| targetStyle: any, // CSSStyleDeclaration - doesn't allow numbers to be assigned to properties | ||
| styleProp?: MotionStyle | ||
| ) { | ||
| if (!this.instance || this.isSVG) return | ||
|
|
||
| if (!this.isVisible) { | ||
| /** | ||
| * The preceding style render may have re-written | ||
| * visibility, so always re-hide. wroteHidden tracks that | ||
| * a restoring write is needed when the node is shown. | ||
| */ | ||
| targetStyle.visibility = "hidden" | ||
| this.wroteHidden = true | ||
| return | ||
| } | ||
|
|
||
| if (this.wroteHidden) { | ||
| this.wroteHidden = false | ||
| targetStyle.visibility = "" | ||
| } | ||
|
|
||
| const transformTemplate = this.getTransformTemplate() | ||
|
|
||
| if (this.needsReset) { | ||
| this.needsReset = false | ||
|
|
||
| targetStyle.visibility = "" | ||
| this.clearRenderCache() | ||
| targetStyle.opacity = "" | ||
| targetStyle.pointerEvents = | ||
| resolveMotionValue(styleProp?.pointerEvents) || "" | ||
|
|
@@ -2025,13 +2088,12 @@ export function createProjectionNode<I>({ | |
| ? transformTemplate({}, "") | ||
| : "none" | ||
| this.hasProjected = false | ||
| this.clearRenderCache() | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| targetStyle.visibility = "" | ||
|
|
||
| const valuesToRender = lead.animationValues || lead.latestValues | ||
| this.applyTransformsToTarget() | ||
|
|
||
|
|
@@ -2045,12 +2107,29 @@ export function createProjectionNode<I>({ | |
| transform = transformTemplate(valuesToRender, transform) | ||
| } | ||
|
|
||
| targetStyle.transform = transform | ||
| /** | ||
| * The following writes are memoized against the last | ||
| * projection-rendered value as CSSOM setter calls are the | ||
| * dominant cost of rendering large numbers of projection | ||
| * nodes every frame. Projection owns these styles while | ||
| * projecting (see willProjectTransform), so the caches can't | ||
| * be invalidated by the regular style render. | ||
| */ | ||
| if (transform !== this.renderedTransform) { | ||
| this.renderedTransform = targetStyle.transform = transform | ||
| } | ||
|
Comment on lines
+2118
to
+2120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Knowledge Base Used: |
||
|
|
||
| const { x, y } = this.projectionDelta | ||
| targetStyle.transformOrigin = `${x.origin * 100}% ${ | ||
| y.origin * 100 | ||
| }% 0` | ||
| if ( | ||
| x.origin !== this.renderedOriginX || | ||
| y.origin !== this.renderedOriginY | ||
| ) { | ||
| this.renderedOriginX = x.origin | ||
| this.renderedOriginY = y.origin | ||
| targetStyle.transformOrigin = `${x.origin * 100}% ${ | ||
| y.origin * 100 | ||
| }% 0` | ||
| } | ||
|
|
||
| if (lead.animationValues) { | ||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds stateful style caches, several lifecycle invalidation paths, and conditional suppression of normal DOM writes without adding automated coverage, leaving stale-style and ownership regressions undetected despite the repository requirement to test every new feature.
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!