Skip to content

Commit

Permalink
Merge pull request #2845 from framer/fix/ssr-will-change
Browse files Browse the repository at this point in the history
Removing `will-change` from SSR
  • Loading branch information
mergetron[bot] authored Oct 25, 2024
2 parents d905a99 + 0325534 commit 1cf39c7
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 101 deletions.
8 changes: 4 additions & 4 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,23 @@
"bundlesize": [
{
"path": "./dist/size-rollup-motion.js",
"maxSize": "33.96 kB"
"maxSize": "33.8 kB"
},
{
"path": "./dist/size-rollup-m.js",
"maxSize": "5.9 kB"
"maxSize": "5.7 kB"
},
{
"path": "./dist/size-rollup-dom-animation.js",
"maxSize": "17 kB"
"maxSize": "16.95 kB"
},
{
"path": "./dist/size-rollup-dom-max.js",
"maxSize": "29.1 kB"
},
{
"path": "./dist/size-rollup-animate.js",
"maxSize": "18 kB"
"maxSize": "17.9 kB"
},
{
"path": "./dist/size-rollup-scroll.js",
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/src/motion/__tests__/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function runTests(render: (components: any) => string) {
)

expect(div).toBe(
'<div style="will-change:transform;transform:translateX(100px) translateY(200px)"></div>'
'<div style="transform:translateX(100px) translateY(200px)"></div>'
)
})

Expand All @@ -165,7 +165,7 @@ function runTests(render: (components: any) => string) {
)

expect(customElement).toBe(
'<element-test style="will-change:transform;transform:translateX(100px) translateY(200px)"></element-test>'
'<element-test style="transform:translateX(100px) translateY(200px)"></element-test>'
)
})

Expand Down
5 changes: 0 additions & 5 deletions packages/framer-motion/src/motion/__tests__/variant.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -817,11 +817,7 @@ describe("animate prop as variant", () => {
const promise = new Promise((resolve) => {
let latest = {}

let frameCount = 0

const onUpdate = (l: { [key: string]: number | string }) => {
frameCount++
if (frameCount === 2) expect(l.willChange).toBe("transform")
latest = l
}

Expand All @@ -842,7 +838,6 @@ describe("animate prop as variant", () => {
})

return expect(promise).resolves.toEqual({
willChange: "transform",
x: 100,
y: 100,
})
Expand Down
99 changes: 28 additions & 71 deletions packages/framer-motion/src/motion/utils/use-visual-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import {
isControllingVariants as checkIsControllingVariants,
isVariantNode as checkIsVariantNode,
} from "../../render/utils/is-controlling-variants"
import { TargetAndTransition } from "../../types"
import { getWillChangeName } from "../../value/use-will-change/get-will-change-name"

export interface VisualState<Instance, RenderState> {
renderState: RenderState
Expand All @@ -29,7 +27,6 @@ export type UseVisualState<Instance, RenderState> = (
) => VisualState<Instance, RenderState>

export interface UseVisualStateConfig<Instance, RenderState> {
applyWillChange?: boolean
scrapeMotionValuesFromProps: ScrapeMotionValuesFromProps
createRenderState: () => RenderState
onMount?: (
Expand All @@ -41,22 +38,19 @@ export interface UseVisualStateConfig<Instance, RenderState> {

function makeState<I, RS>(
{
applyWillChange = false,
scrapeMotionValuesFromProps,
createRenderState,
onMount,
}: UseVisualStateConfig<I, RS>,
props: MotionProps,
context: MotionContextProps,
presenceContext: PresenceContextProps | null,
isStatic: boolean
presenceContext: PresenceContextProps | null
) {
const state: VisualState<I, RS> = {
latestValues: makeLatestValues(
props,
context,
presenceContext,
isStatic ? false : applyWillChange,
scrapeMotionValuesFromProps
),
renderState: createRenderState(),
Expand All @@ -74,41 +68,18 @@ export const makeUseVisualState =
(props: MotionProps, isStatic: boolean): VisualState<I, RS> => {
const context = useContext(MotionContext)
const presenceContext = useContext(PresenceContext)
const make = () =>
makeState(config, props, context, presenceContext, isStatic)
const make = () => makeState(config, props, context, presenceContext)

return isStatic ? make() : useConstant(make)
}

function forEachDefinition(
props: MotionProps,
definition: MotionProps["animate"] | MotionProps["initial"],
callback: (
target: TargetAndTransition,
transitionEnd: ResolvedValues
) => void
) {
const list = Array.isArray(definition) ? definition : [definition]
for (let i = 0; i < list.length; i++) {
const resolved = resolveVariantFromProps(props, list[i] as any)
if (resolved) {
const { transitionEnd, transition, ...target } = resolved
callback(target, transitionEnd as ResolvedValues)
}
}
}

function makeLatestValues(
props: MotionProps,
context: MotionContextProps,
presenceContext: PresenceContextProps | null,
shouldApplyWillChange: boolean,
scrapeMotionValues: ScrapeMotionValuesFromProps
) {
const values: ResolvedValues = {}
const willChange = new Set()
const applyWillChange =
shouldApplyWillChange && props.style?.willChange === undefined

const motionValues = scrapeMotionValues(props, {})
for (const key in motionValues) {
Expand Down Expand Up @@ -141,49 +112,35 @@ function makeLatestValues(
typeof variantToSet !== "boolean" &&
!isAnimationControls(variantToSet)
) {
forEachDefinition(props, variantToSet, (target, transitionEnd) => {
for (const key in target) {
let valueTarget = target[key as keyof typeof target]

if (Array.isArray(valueTarget)) {
/**
* Take final keyframe if the initial animation is blocked because
* we want to initialise at the end of that blocked animation.
*/
const index = isInitialAnimationBlocked
? valueTarget.length - 1
: 0
valueTarget = valueTarget[index]
}

if (valueTarget !== null) {
values[key] = valueTarget as string | number
}
}
for (const key in transitionEnd) {
values[key] = transitionEnd[
key as keyof typeof transitionEnd
] as string | number
}
})
}

// Add animating values to will-change
if (applyWillChange) {
if (animate && initial !== false && !isAnimationControls(animate)) {
forEachDefinition(props, animate, (target) => {
for (const name in target) {
const memberName = getWillChangeName(name)
const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet]
for (let i = 0; i < list.length; i++) {
const resolved = resolveVariantFromProps(props, list[i] as any)
if (resolved) {
const { transitionEnd, transition, ...target } = resolved
for (const key in target) {
let valueTarget = target[key as keyof typeof target]

if (Array.isArray(valueTarget)) {
/**
* Take final keyframe if the initial animation is blocked because
* we want to initialise at the end of that blocked animation.
*/
const index = isInitialAnimationBlocked
? valueTarget.length - 1
: 0
valueTarget = valueTarget[index]
}

if (memberName) {
willChange.add(memberName)
if (valueTarget !== null) {
values[key] = valueTarget as string | number
}
}
})
}

if (willChange.size) {
values.willChange = Array.from(willChange).join(",")
for (const key in transitionEnd) {
values[key] = transitionEnd[
key as keyof typeof transitionEnd
] as string | number
}
}
}
}

Expand Down
6 changes: 0 additions & 6 deletions packages/framer-motion/src/render/VisualElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,6 @@ export abstract class VisualElement<
projection?: IProjectionNode
): void

/**
* If true, will-change will be applied to the element. Only HTMLVisualElements
* currently support this.
*/
applyWillChange = false

/**
* If the component child is provided as a motion value, handle subscriptions
* with the renderer-specific VisualElement.
Expand Down
2 changes: 0 additions & 2 deletions packages/framer-motion/src/render/html/HTMLVisualElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export class HTMLVisualElement extends DOMVisualElement<
> {
type = "html"

applyWillChange = true

readValueFromInstance(
instance: HTMLElement,
key: string
Expand Down
1 change: 0 additions & 1 deletion packages/framer-motion/src/render/html/config-motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const htmlMotionConfig: Partial<
MotionComponentConfig<HTMLElement, HTMLRenderState>
> = {
useVisualState: makeUseVisualState({
applyWillChange: true,
scrapeMotionValuesFromProps,
createRenderState: createHtmlRenderState,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,5 @@ export function scrapeMotionValuesFromProps(
}
}

/**
* If the willChange style has been manually set as a string, set
* applyWillChange to false to prevent it from automatically being applied.
*/
if (visualElement && style && typeof style.willChange === "string") {
visualElement.applyWillChange = false
}

return newValues
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { renderToString, renderToStaticMarkup } from "react-dom/server"
import { MotionConfig, motion } from "../../../"
import { motionValue } from "../../../value"

function runTests(render: (components: any) => string) {
test("will-change not applied", () => {
const div = render(
<motion.div
initial={
{
x: 100,
clipPath: "inset(10px)",
"--color": "#000",
} as any
}
animate={
{
x: 200,
clipPath: "inset(20px)",
"--color": "#fff",
} as any
}
/>
)

expect(div).toBe(
`<div style="--color:#000;clip-path:inset(10px);transform:translateX(100px)"></div>`
)
})

test("will-change not set in static mode", () => {
const div = render(
<MotionConfig isStatic>
<motion.div
initial={{ x: 100, clipPath: "inset(10px)" } as any}
animate={{ x: 200, clipPath: "inset(20px)" } as any}
/>
</MotionConfig>
)

expect(div).toBe(
`<div style="clip-path:inset(10px);transform:translateX(100px)"></div>`
)
})

test("will-change manually set", () => {
const div = render(
<motion.div
initial={{ x: 100, "--color": "#000" } as any}
animate={{ x: 200 }}
style={{ willChange: "opacity" }}
/>
)

expect(div).toBe(
`<div style="will-change:opacity;--color:#000;transform:translateX(100px)"></div>`
)
})

test("will-change manually set without animated values", () => {
const div = render(<motion.div style={{ willChange: "opacity" }} />)

expect(div).toBe(`<div style="will-change:opacity"></div>`)
})

test("will-change not set without animated values", () => {
const div = render(<motion.div style={{}} />)

expect(div).toBe(`<div></div>`)
})

test("Externally defined MotionValues not automatically added to will-change", () => {
const opacity = motionValue(0.5)
const div = render(<motion.div style={{ opacity }} />)

expect(div).toBe(`<div style="opacity:0.5"></div>`)
})

test("will-change manually set by MotionValue", () => {
const willChange = motionValue("opacity")
const div = render(
<motion.div
initial={{ x: 100, "--color": "#000" } as any}
animate={{ x: 200 }}
style={{ willChange }}
/>
)

expect(div).toBe(
`<div style="--color:#000;will-change:opacity;transform:translateX(100px)"></div>`
)
})

test("will-change correctly not applied when isStatic", () => {
const div = render(
<MotionConfig isStatic>
<motion.div
initial={{ x: 100, "--color": "#000" } as any}
animate={{ x: 200 }}
/>
</MotionConfig>
)

expect(div).toBe(
`<div style="--color:#000;transform:translateX(100px)"></div>`
)
})
}

describe("render", () => {
runTests(renderToString)
})

describe("renderToStaticMarkup", () => {
runTests(renderToStaticMarkup)
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export function addValueToWillChange(
visualElement: VisualElement,
key: string
) {
if (!visualElement.applyWillChange) return

const willChange = visualElement.getValue("willChange")

/**
Expand Down

0 comments on commit 1cf39c7

Please sign in to comment.