From 6de5049cdf6705be5776de502484beaa9a3a7fd3 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Thu, 28 May 2026 15:43:04 +0200 Subject: [PATCH] Strip unused stats internals from shipped bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stats module shipped per-frame counters and a reportStats() summariser that no external consumer reads — only the HTML projection perf tests in dev/html use statsBuffer (and only the layoutProjection metrics). Removed: - record() per-frame callback + frame.postRender scheduling - summarise(), mean(), msToFps(), reportStats(), Summary/StatsSummary types - activeAnimations counters + mutations in JSAnimation, start-waapi-animation, create-projection-node (mainThread/waapi/layout) — nothing reads them - stepName param + numCalls in render-step (per-step counter pushes); nothing outside the removed summariser consumed them - frameloop / animations shape from the stats buffer; recordStats now only initialises layoutProjection Kept (HTML projection harness still uses these via window.Projection): - recordStats() — flips statsBuffer into recording mode for layoutProjection - statsBuffer + addProjectionMetrics — fed by create-projection-node gates - The 4 `if (statsBuffer.value)` / `if (statsBuffer.addProjectionMetrics)` gates in create-projection-node Bundle deltas (gzipped): - motion-dom.js: 38921 -> 38342 (-579 B) - framer-motion.js: 60694 -> 60090 (-604 B) - dom.js: 44678 -> 44091 (-587 B) - motion-dom.dev.js: 90813 -> 89959 (-854 B) - stats/index.mjs: 997 -> 350 (-647 B) Co-Authored-By: Claude Opus 4.7 --- .../motion-dom/src/animation/JSAnimation.ts | 3 - .../animation/waapi/start-waapi-animation.ts | 16 +- packages/motion-dom/src/frameloop/batcher.ts | 5 +- .../motion-dom/src/frameloop/render-step.ts | 18 +-- packages/motion-dom/src/index.ts | 1 - .../projection/node/create-projection-node.ts | 6 - .../src/stats/__tests__/index.test.ts | 146 ++++-------------- .../motion-dom/src/stats/animation-count.ts | 5 - packages/motion-dom/src/stats/buffer.ts | 8 +- packages/motion-dom/src/stats/index.ts | 121 +-------------- packages/motion-dom/src/stats/types.ts | 40 ++--- 11 files changed, 51 insertions(+), 318 deletions(-) delete mode 100644 packages/motion-dom/src/stats/animation-count.ts diff --git a/packages/motion-dom/src/animation/JSAnimation.ts b/packages/motion-dom/src/animation/JSAnimation.ts index b0a7a1e96a..2f116513e9 100644 --- a/packages/motion-dom/src/animation/JSAnimation.ts +++ b/packages/motion-dom/src/animation/JSAnimation.ts @@ -6,7 +6,6 @@ import { secondsToMilliseconds, } from "motion-utils" import { time } from "../frameloop/sync-time" -import { activeAnimations } from "../stats/animation-count" import { mix } from "../utils/mix" import { Mixer } from "../utils/mix/types" import { frameloopDriver } from "./drivers/frame" @@ -91,7 +90,6 @@ export class JSAnimation constructor(options: ValueAnimationOptions) { super() - activeAnimations.mainThread++ this.options = options this.initAnimation() @@ -526,7 +524,6 @@ export class JSAnimation this.state = "idle" this.stopDriver() this.startTime = this.holdTime = null - activeAnimations.mainThread-- } private stopDriver() { diff --git a/packages/motion-dom/src/animation/waapi/start-waapi-animation.ts b/packages/motion-dom/src/animation/waapi/start-waapi-animation.ts index da25356192..3ac584e277 100644 --- a/packages/motion-dom/src/animation/waapi/start-waapi-animation.ts +++ b/packages/motion-dom/src/animation/waapi/start-waapi-animation.ts @@ -1,5 +1,3 @@ -import { activeAnimations } from "../../stats/animation-count" -import { statsBuffer } from "../../stats/buffer" import { ValueKeyframesDefinition, ValueTransition } from "../types" import { mapEasingToNativeEasing } from "./easing/map-easing" @@ -29,10 +27,6 @@ export function startWaapiAnimation( */ if (Array.isArray(easing)) keyframeOptions.easing = easing - if (statsBuffer.value) { - activeAnimations.waapi++ - } - const options: KeyframeAnimationOptions = { delay, duration, @@ -44,13 +38,5 @@ export function startWaapiAnimation( if (pseudoElement) options.pseudoElement = pseudoElement - const animation = element.animate(keyframeOptions, options) - - if (statsBuffer.value) { - animation.finished.finally(() => { - activeAnimations.waapi-- - }) - } - - return animation + return element.animate(keyframeOptions, options) } diff --git a/packages/motion-dom/src/frameloop/batcher.ts b/packages/motion-dom/src/frameloop/batcher.ts index 55d0c4bcc1..1e4a36bc78 100644 --- a/packages/motion-dom/src/frameloop/batcher.ts +++ b/packages/motion-dom/src/frameloop/batcher.ts @@ -21,10 +21,7 @@ export function createRenderBatcher( const flagRunNextFrame = () => (runNextFrame = true) const steps = stepsOrder.reduce((acc, key) => { - acc[key] = createRenderStep( - flagRunNextFrame, - allowKeepAlive ? key : undefined - ) + acc[key] = createRenderStep(flagRunNextFrame) return acc }, {} as Steps) diff --git a/packages/motion-dom/src/frameloop/render-step.ts b/packages/motion-dom/src/frameloop/render-step.ts index 7ef65aba94..0802b85c51 100644 --- a/packages/motion-dom/src/frameloop/render-step.ts +++ b/packages/motion-dom/src/frameloop/render-step.ts @@ -1,11 +1,6 @@ -import { statsBuffer } from "../stats/buffer" -import { StepNames } from "./order" import { FrameData, Process, Step } from "./types" -export function createRenderStep( - runNextFrame: () => void, - stepName?: StepNames -): Step { +export function createRenderStep(runNextFrame: () => void): Step { /** * We create and reuse two queues, one to queue jobs for the current frame * and one for the next. We reuse to avoid triggering GC after x frames. @@ -32,15 +27,12 @@ export function createRenderStep( isProcessing: false, } - let numCalls = 0 - function triggerCallback(callback: Process) { if (toKeepAlive.has(callback)) { step.schedule(callback) runNextFrame() } - numCalls++ callback(latestFrameData) } @@ -93,14 +85,6 @@ export function createRenderStep( // Execute this frame thisFrame.forEach(triggerCallback) - /** - * If we're recording stats then - */ - if (stepName && statsBuffer.value) { - statsBuffer.value.frameloop[stepName].push(numCalls) - } - numCalls = 0 - // Clear the frame so no callbacks remain. This is to avoid // memory leaks should this render step not run for a while. thisFrame.clear() diff --git a/packages/motion-dom/src/index.ts b/packages/motion-dom/src/index.ts index 945bb7ad52..7711f374c9 100644 --- a/packages/motion-dom/src/index.ts +++ b/packages/motion-dom/src/index.ts @@ -101,7 +101,6 @@ export * from "./resize" export * from "./scroll/observe" export * from "./stats" -export * from "./stats/animation-count" export * from "./stats/buffer" export * from "./stats/types" diff --git a/packages/motion-dom/src/projection/node/create-projection-node.ts b/packages/motion-dom/src/projection/node/create-projection-node.ts index 0c47c3bb5d..fdb7227316 100644 --- a/packages/motion-dom/src/projection/node/create-projection-node.ts +++ b/packages/motion-dom/src/projection/node/create-projection-node.ts @@ -26,7 +26,6 @@ import { HTMLVisualElement } from "../../render/html/HTMLVisualElement" import type { ResolvedValues } from "../../render/types" import { scaleCorrectors } from "../../render/utils/is-forced-motion-value" import type { MotionStyle, VisualElement } from "../../render/VisualElement" -import { activeAnimations } from "../../stats/animation-count" import { statsBuffer } from "../../stats/buffer" import { delay } from "../../utils/delay" import { isSVGElement } from "../../utils/is-svg-element" @@ -1733,7 +1732,6 @@ export function createProjectionNode({ this.pendingAnimation = frame.update(() => { globalProjectionState.hasAnimatedSinceResize = true - activeAnimations.layout++ this.motionValue ||= motionValue(0) this.motionValue.jump(0, false) @@ -1748,11 +1746,7 @@ export function createProjectionNode({ this.mixTargetDelta(latest) options.onUpdate && options.onUpdate(latest) }, - onStop: () => { - activeAnimations.layout-- - }, onComplete: () => { - activeAnimations.layout-- options.onComplete && options.onComplete() this.completeAnimation() }, diff --git a/packages/motion-dom/src/stats/__tests__/index.test.ts b/packages/motion-dom/src/stats/__tests__/index.test.ts index d0745aa2e9..18191135d1 100644 --- a/packages/motion-dom/src/stats/__tests__/index.test.ts +++ b/packages/motion-dom/src/stats/__tests__/index.test.ts @@ -1,130 +1,46 @@ -import { MotionGlobalConfig } from "motion-utils" import { recordStats } from ".." -import { frame, frameData } from "../../frameloop" - -MotionGlobalConfig.useManualTiming = true +import { statsBuffer } from "../buffer" describe("recordStats", () => { - it("should throw an error if stats are already being measured", () => { - expect(() => { - recordStats() - recordStats() - }).toThrow() + beforeEach(() => { + statsBuffer.value = null + statsBuffer.addProjectionMetrics = null }) - it("should return the correct stats", async () => { - return new Promise((resolve) => { - const record = recordStats() - - frameData.timestamp = 15 - frameData.delta = 1000 / 60 - - frame.update(() => {}) - - frame.render(() => { - queueMicrotask(() => { - const stats = record() - - const { - rate, - read, - resolveKeyframes, - update, - preRender, - render, - postRender, - } = stats.frameloop - - expect(rate.min).toEqual(60) - expect(rate.max).toEqual(60) - expect(rate.avg).toEqual(60) - expect(read.min).toEqual(0) - expect(read.max).toEqual(0) - expect(read.avg).toEqual(0) - expect(resolveKeyframes.min).toEqual(0) - expect(resolveKeyframes.max).toEqual(0) - expect(resolveKeyframes.avg).toEqual(0) - expect(update.min).toEqual(1) - expect(update.max).toEqual(1) - expect(update.avg).toEqual(1) - expect(preRender.min).toEqual(0) - expect(preRender.max).toEqual(0) - expect(preRender.avg).toEqual(0) - expect(render.min).toEqual(1) - expect(render.max).toEqual(1) - expect(render.avg).toEqual(1) + it("throws if stats are already being measured", () => { + recordStats() + expect(() => recordStats()).toThrow() + }) - // postRender always at least 1 as stats itself uses a postRender step - expect(postRender.min).toEqual(1) - expect(postRender.max).toEqual(1) - expect(postRender.avg).toEqual(1) + it("initializes the layout projection buffer", () => { + recordStats() - resolve() - }) - }) + expect(statsBuffer.value).not.toBeNull() + expect(statsBuffer.value!.layoutProjection).toEqual({ + nodes: [], + calculatedTargetDeltas: [], + calculatedProjections: [], }) }) - it("should return the correct stats across multiple frames", async () => { - return new Promise((resolve) => { - const record = recordStats() - - frameData.timestamp = 15 - frameData.delta = 1000 / 60 + it("addProjectionMetrics appends per-frame metrics", () => { + recordStats() - frame.update(() => {}) - - frame.render(() => {}) - - frame.postRender(() => { - frameData.timestamp = 30 - frameData.delta = 1000 / 30 - - frame.update(() => {}) - frame.update(() => {}) - frame.update(() => {}) - frame.render(() => { - queueMicrotask(() => { - const stats = record() - - const { - rate, - read, - resolveKeyframes, - update, - preRender, - render, - postRender, - } = stats.frameloop - - expect(rate.min).toEqual(30) - expect(rate.max).toEqual(60) - expect(rate.avg).toEqual(40) - expect(read.min).toEqual(0) - expect(read.max).toEqual(0) - expect(read.avg).toEqual(0) - expect(resolveKeyframes.min).toEqual(0) - expect(resolveKeyframes.max).toEqual(0) - expect(resolveKeyframes.avg).toEqual(0) - expect(update.min).toEqual(1) - expect(update.max).toEqual(3) - expect(update.avg).toEqual(2) - expect(preRender.min).toEqual(0) - expect(preRender.max).toEqual(0) - expect(preRender.avg).toEqual(0) - expect(render.min).toEqual(1) - expect(render.max).toEqual(1) - expect(render.avg).toEqual(1) - - // postRender always at least 1 as stats itself uses a postRender step - expect(postRender.min).toEqual(1) - expect(postRender.max).toEqual(2) - expect(postRender.avg).toEqual(1.5) + statsBuffer.addProjectionMetrics!({ + nodes: 3, + calculatedTargetDeltas: 2, + calculatedProjections: 1, + }) + statsBuffer.addProjectionMetrics!({ + nodes: 4, + calculatedTargetDeltas: 0, + calculatedProjections: 2, + }) - resolve() - }) - }) - }) + expect(statsBuffer.value!.layoutProjection).toEqual({ + nodes: [3, 4], + calculatedTargetDeltas: [2, 0], + calculatedProjections: [1, 2], }) }) }) diff --git a/packages/motion-dom/src/stats/animation-count.ts b/packages/motion-dom/src/stats/animation-count.ts deleted file mode 100644 index d1e1354764..0000000000 --- a/packages/motion-dom/src/stats/animation-count.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const activeAnimations = { - layout: 0, - mainThread: 0, - waapi: 0, -} diff --git a/packages/motion-dom/src/stats/buffer.ts b/packages/motion-dom/src/stats/buffer.ts index bebb537e6c..46269d49fe 100644 --- a/packages/motion-dom/src/stats/buffer.ts +++ b/packages/motion-dom/src/stats/buffer.ts @@ -1,4 +1,4 @@ -import type { StatsRecording } from "./types" +import type { LayoutProjectionMetrics, StatsRecording } from "./types" export type InactiveStatsBuffer = { value: null @@ -7,11 +7,7 @@ export type InactiveStatsBuffer = { export type ActiveStatsBuffer = { value: StatsRecording - addProjectionMetrics: (metrics: { - nodes: number - calculatedTargetDeltas: number - calculatedProjections: number - }) => void + addProjectionMetrics: (metrics: LayoutProjectionMetrics) => void } export const statsBuffer: InactiveStatsBuffer | ActiveStatsBuffer = { diff --git a/packages/motion-dom/src/stats/index.ts b/packages/motion-dom/src/stats/index.ts index 281c6dc385..22b5e88ac1 100644 --- a/packages/motion-dom/src/stats/index.ts +++ b/packages/motion-dom/src/stats/index.ts @@ -1,128 +1,19 @@ -import { cancelFrame, frame, frameData } from "../frameloop" -import { activeAnimations } from "./animation-count" import { ActiveStatsBuffer, statsBuffer } from "./buffer" -import { StatsSummary, Summary } from "./types" - -function record() { - const { value } = statsBuffer - - if (value === null) { - cancelFrame(record) - return - } - - value.frameloop.rate.push(frameData.delta) - value.animations.mainThread.push(activeAnimations.mainThread) - value.animations.waapi.push(activeAnimations.waapi) - value.animations.layout.push(activeAnimations.layout) -} - -function mean(values: number[]) { - return values.reduce((acc, value) => acc + value, 0) / values.length -} - -function summarise( - values: number[], - calcAverage: (allValues: number[]) => number = mean -): Summary { - if (values.length === 0) { - return { - min: 0, - max: 0, - avg: 0, - } - } - - return { - min: Math.min(...values), - max: Math.max(...values), - avg: calcAverage(values), - } -} - -const msToFps = (ms: number) => Math.round(1000 / ms) function clearStatsBuffer() { statsBuffer.value = null statsBuffer.addProjectionMetrics = null } -function reportStats(): StatsSummary { - const { value } = statsBuffer - - if (!value) { - throw new Error("Stats are not being measured") - } - - clearStatsBuffer() - cancelFrame(record) - - const summary = { - frameloop: { - setup: summarise(value.frameloop.setup), - rate: summarise(value.frameloop.rate), - read: summarise(value.frameloop.read), - resolveKeyframes: summarise(value.frameloop.resolveKeyframes), - preUpdate: summarise(value.frameloop.preUpdate), - update: summarise(value.frameloop.update), - preRender: summarise(value.frameloop.preRender), - render: summarise(value.frameloop.render), - postRender: summarise(value.frameloop.postRender), - }, - animations: { - mainThread: summarise(value.animations.mainThread), - waapi: summarise(value.animations.waapi), - layout: summarise(value.animations.layout), - }, - layoutProjection: { - nodes: summarise(value.layoutProjection.nodes), - calculatedTargetDeltas: summarise( - value.layoutProjection.calculatedTargetDeltas - ), - calculatedProjections: summarise( - value.layoutProjection.calculatedProjections - ), - }, - } - - /** - * Convert the rate to FPS - */ - const { rate } = summary.frameloop - rate.min = msToFps(rate.min) - rate.max = msToFps(rate.max) - rate.avg = msToFps(rate.avg) - // Swap these as the min and max are inverted when converted to FPS - ;[rate.min, rate.max] = [rate.max, rate.min] - - return summary -} - export function recordStats() { if (statsBuffer.value) { clearStatsBuffer() throw new Error("Stats are already being measured") } - const newStatsBuffer = statsBuffer as unknown as ActiveStatsBuffer + const buffer = statsBuffer as unknown as ActiveStatsBuffer - newStatsBuffer.value = { - frameloop: { - setup: [], - rate: [], - read: [], - resolveKeyframes: [], - preUpdate: [], - update: [], - preRender: [], - render: [], - postRender: [], - }, - animations: { - mainThread: [], - waapi: [], - layout: [], - }, + buffer.value = { layoutProjection: { nodes: [], calculatedTargetDeltas: [], @@ -130,8 +21,8 @@ export function recordStats() { }, } - newStatsBuffer.addProjectionMetrics = (metrics) => { - const { layoutProjection } = newStatsBuffer.value + buffer.addProjectionMetrics = (metrics) => { + const { layoutProjection } = buffer.value layoutProjection.nodes.push(metrics.nodes) layoutProjection.calculatedTargetDeltas.push( metrics.calculatedTargetDeltas @@ -140,8 +31,4 @@ export function recordStats() { metrics.calculatedProjections ) } - - frame.postRender(record, true) - - return reportStats } diff --git a/packages/motion-dom/src/stats/types.ts b/packages/motion-dom/src/stats/types.ts index 02eaaf1bea..b93ceaae08 100644 --- a/packages/motion-dom/src/stats/types.ts +++ b/packages/motion-dom/src/stats/types.ts @@ -1,33 +1,15 @@ -import { StepNames } from "../frameloop/order" - -export interface Summary { - min: number - max: number - avg: number +export interface LayoutProjectionMetrics { + nodes: number + calculatedTargetDeltas: number + calculatedProjections: number } -type FrameloopStatNames = "rate" | StepNames - -export interface Stats { - frameloop: { - [key in FrameloopStatNames]: T - } - animations: { - mainThread: T - waapi: T - layout: T - } - layoutProjection: { - nodes: T - calculatedTargetDeltas: T - calculatedProjections: T - } +export interface LayoutProjectionStats { + nodes: number[] + calculatedTargetDeltas: number[] + calculatedProjections: number[] } -export type StatsBuffer = number[] - -export type FrameStats = Stats - -export type StatsRecording = Stats - -export type StatsSummary = Stats +export interface StatsRecording { + layoutProjection: LayoutProjectionStats +}