Skip to content

Strip unused stats internals from shipped bundle (~580 B gz / bundle)#3744

Open
mattgperry wants to merge 1 commit into
mainfrom
cleanup/strip-unused-stats
Open

Strip unused stats internals from shipped bundle (~580 B gz / bundle)#3744
mattgperry wants to merge 1 commit into
mainfrom
cleanup/strip-unused-stats

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Why

The runtime stats system shipped:

  • a record() callback scheduled every postRender frame,
  • a reportStats() summariser (summarise, mean, msToFps, FPS
    conversion),
  • activeAnimations.mainThread/waapi/layout counters mutated by every
    JS animation construct/teardown, every WAAPI start/finish, and the
    layout-animation lifecycle,
  • per-step call-count pushes in render-step,
  • Summary / StatsSummary / FrameStats type exports.

Search across the monorepo and the public-debug entries (framer-motion/debug, framer-motion/projection) shows the only live consumer is dev/html/src/imports/script-assert.js, which reads statsBuffer.value.layoutProjection.{nodes,calculatedTargetDeltas,calculatedProjections} to make assertions in the HTML projection perf tests (perf-single, perf-neighbours, perf-parent-child, perf-shared-deep, etc.).

It calls recordStats() to flip the flag, then reads the projection counters directly. It never calls the returned reportStats, never touches frameloop / animations shape, never reads activeAnimations.

So everything else was shipping to users for no consumer. This strip keeps that test harness functional today while removing the rest from the bundle.

What stayed

  • recordStats() — now just initialises statsBuffer.value.layoutProjection = {...} and installs addProjectionMetrics. No frame is scheduled.
  • statsBuffer + addProjectionMetrics callback.
  • The 4 if (statsBuffer.value) / if (statsBuffer.addProjectionMetrics) gates in create-projection-node that push projection counts.
  • Re-exports of recordStats / statsBuffer from framer-motion/projection (the entry the HTML harness reaches via window.Projection).

What went

Removed Why
record() per-frame callback + frame.postRender(record, true) Pushed frameloop.rate and animations.* arrays nothing reads
reportStats(), summarise, summariseAll, mean, msToFps, FPS conversion Returned value never read
clearStatsBuffer (kept only as the internal narrowing trick) only used by reportStats
activeAnimations + counter increments in JSAnimation, start-waapi-animation, create-projection-node only fed animations.* arrays that nothing reads
stepName param + numCalls + the stats push in render-step only fed frameloop.* arrays
allowKeepAlive ? key : undefined in batcher second arg no longer exists
frameloop + animations shape from the stats buffer only layoutProjection is read
Summary, StatsSummary, FrameStats, Stats<T>, StatsBuffer types + the barrel re-export of animation-count unused
packages/motion-dom/src/stats/animation-count.ts file deleted

Bundle deltas (gzipped, gzip -9)

Output Before After Δ
motion-dom/dist/motion-dom.js 38921 38342 −579
motion-dom/dist/motion-dom.dev.js 90813 89959 −854
motion-dom/dist/cjs/index.js 89109 88279 −830
motion-dom/dist/es/stats/index.mjs 997 350 −647
framer-motion/dist/framer-motion.js 60694 60090 −604
framer-motion/dist/dom.js 44678 44091 −587
framer-motion/dist/dom-mini.js 4979 4979 0 (no stats)

~580 B gz off every user-facing bundle that includes the animation engine.

Public API

recordStats is still exported (HTML test relies on it via framer-motion/projection) but its signature narrowed: it no longer returns a reportStats callback. Anyone in the wild who was calling const stop = recordStats(); stop() to get a summary will need to switch — but the prior behaviour summarised data nobody else was collecting, so it's hard to imagine a real consumer.

The Summary / StatsSummary / FrameStats types are removed from the public type surface.

Follow-up

Future change should move the HTML projection harness off statsBuffer (e.g. expose a per-node hook to count propagateDirtyNodes / resolveTargetDelta / calcProjection invocations) so the 4 remaining statsBuffer gates in create-projection-node can be dropped too — at which point stats/ can be deleted entirely. Out of scope here to keep the diff focused.

Supersedes

#3742 — that PR refactored summarise() to dedupe its 15 call sites; this PR removes summarise() altogether. Close #3742 when this lands.

Verification

  • yarn build clean.
  • yarn test — 797 passed, 7 skipped, 0 failed (incl. the rewritten projection-metric Jest test in motion-dom/src/stats/__tests__/index.test.ts).
  • HTML projection perf harness manually traced: recordStats() still initialises statsBuffer.value.layoutProjection, the 4 projection-node gates still push, and script-assert.js's checkFrame reads the same array shape it did before.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 28, 2026

Greptile Summary

This PR strips unused stats infrastructure from the shipped bundle by removing the record() per-frame callback, reportStats() and its helpers, activeAnimations counters, and the frameloop/animations shapes from the stats buffer — none of which had any live consumer outside dev internals. The statsBuffer and addProjectionMetrics path for layout projection metrics is retained because the HTML projection perf test harness reads it directly.

  • Deleted animation-count.ts and all counter increment/decrement sites in JSAnimation, start-waapi-animation, and create-projection-node.
  • Simplified recordStats() to only initialise statsBuffer.value.layoutProjection and install addProjectionMetrics; the function no longer returns a callback, which is a narrow public-API break acknowledged in the PR description.
  • render-step.ts loses the step-name and call-count tracking path; batcher.ts removes the now-dead second argument to createRenderStep.

Confidence Score: 5/5

Clean dead-code removal with no behavioral changes to the animation engine; the only retained path (layoutProjection metrics) is unchanged.

Every removed call site had a single purpose — incrementing or reporting counters that nothing outside the dev test harness ever read. The one live consumer (HTML projection perf tests) relies solely on the layoutProjection arrays and the addProjectionMetrics callback, both of which are preserved verbatim. The onStop removal in create-projection-node is safe because that handler exclusively decremented activeAnimations.layout. Tests are updated and passing.

No files require special attention.

Important Files Changed

Filename Overview
packages/motion-dom/src/stats/index.ts Core stats module stripped to only recordStats() + addProjectionMetrics closure; all reporting/summarising helpers removed. Logic is correct and the clearStatsBuffer-before-throw guard is unchanged.
packages/motion-dom/src/stats/buffer.ts Types narrowed to use LayoutProjectionMetrics from the new types file; shape is correct and consistent with the rest of the changes.
packages/motion-dom/src/stats/types.ts Removed Stats, Summary, StatsSummary, FrameStats, StatsBuffer; replaced with lean LayoutProjectionMetrics / LayoutProjectionStats / StatsRecording interfaces that match exactly what the HTML harness reads.
packages/motion-dom/src/stats/tests/index.test.ts Tests rewritten to cover only the remaining surface (initialisation and per-frame metric appending); beforeEach resets shared mutable buffer correctly.
packages/motion-dom/src/frameloop/render-step.ts stepName param and numCalls counter removed; the step processing loop is otherwise untouched and correct.
packages/motion-dom/src/frameloop/batcher.ts Second argument to createRenderStep removed now that stepName no longer exists; change is mechanical and correct.
packages/motion-dom/src/animation/JSAnimation.ts activeAnimations.mainThread increment/decrement removed from constructor and cleanup(); no functional change to animation behavior.
packages/motion-dom/src/animation/waapi/start-waapi-animation.ts Removed statsBuffer-gated activeAnimations.waapi counter and the animation.finished.finally() subscription; return path simplified to a direct element.animate() call.
packages/motion-dom/src/projection/node/create-projection-node.ts Removed activeAnimations.layout counters from layout animation construct/teardown; onStop callback (which only decremented the counter) removed entirely; onComplete unchanged in behavior.
packages/motion-dom/src/stats/animation-count.ts File deleted; the activeAnimations object it exported is no longer needed anywhere.
packages/motion-dom/src/index.ts Barrel export of animation-count removed; stats, buffer, and types re-exports retained.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["recordStats()"] --> B["statsBuffer.value = { layoutProjection: {...} }"]
    B --> C["statsBuffer.addProjectionMetrics = callback"]

    D["create-projection-node\n(4 guarded sites)"] -->|"if statsBuffer.addProjectionMetrics"| E["addProjectionMetrics(metrics)"]
    E --> F["layoutProjection.nodes.push()\ncalculatedTargetDeltas.push()\ncalculatedProjections.push()"]

    G["HTML test harness\nscript-assert.js"] -->|"reads"| H["statsBuffer.value.layoutProjection\n.nodes / .calculatedTargetDeltas / .calculatedProjections"]

    subgraph REMOVED ["Removed from bundle"]
        R1["record() per-frame callback"]
        R2["activeAnimations counters"]
        R3["reportStats / summarise / mean / msToFps"]
        R4["frameloop + animations shapes"]
        R5["numCalls + stepName in render-step"]
    end

    style REMOVED fill:#fee2e2,stroke:#ef4444
Loading

Reviews (1): Last reviewed commit: "Strip unused stats internals from shippe..." | Re-trigger Greptile

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