Back to Motion

Plan pr-3748: Land "Rewrite animateLayout with batched commits on a shared projection tree"

plans/issues/pr-3748.md

12.41.08.4 KB
Original Source

Plan pr-3748: Land "Rewrite animateLayout with batched commits on a shared projection tree"

Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving on. If anything in "STOP conditions" occurs, stop and report — do not improvise. When done, update this plan's row in plans/issues/README.md.

Drift check (run first): gh pr view 3748 --json state,headRefOid,mergeStateStatus Expected: state: OPEN, headRefOid: 87d61a8e7d304290e7345963a6a27b993c241ae0. If closed/merged or head differs, STOP.

Status

  • Priority: P1
  • Effort: S (the code is done; remaining work is one fixture port + sequencing)
  • Risk: MED (rewrite of the engine behind motion-plus unstable_animateLayout)
  • Depends on: maintainer decision on plan 009 sequencing (see Step 1). Must land BEFORE PR #3749 (see plan pr-3749.md). Closes PR #3747 (see plan pr-3747.md).
  • Category: tech-debt / architecture
  • Planned at: commit 42bfbe3ed, 2026-06-11
  • PR: https://github.com/motiondivision/motion/pull/3748 (branch animate-layout-batched-commits)

Why this matters

This is the chosen iteration of the animateLayout rewrite (the engine driving the real projection engine over plain DOM). Its headline improvement over both the current implementation and the earlier attempt (#3747): builders created in the same synchronous tick flush together — all snapshots are taken before any updateDom runs, then additions/removals reconcile across builders and a single shared root.didUpdate() fires. #3747 (per-builder pipelines, implicit batching via the root's microtask-deduped didUpdate()) lets builder A's updateDom run before builder B snapshots, corrupting B's origin measurements on intersecting trees. Two of #3748's new HTML fixtures fail against the previous implementation — the test-first gate is satisfied.

Additional signal (2026-06-11): the maintainer has already chosen this PR's shared projection tree as the substrate for the vanilla-drag arc — see plans/README.md plans 019–021, where plan 020 lists "PR #3748 merged" as a hard dependency. Landing this PR unblocks that P1 track.

Current state

  • 6 files (+784/−299): 1 source file (packages/motion-dom/src/layout/LayoutAnimationBuilder.ts — total rewrite), 4 new HTML fixtures (dev/html/public/animate-layout/: parallel-calls-disjoint.html, parallel-calls-intersecting.html, parallel-calls-shared-element.html, data-layout-true.html), plus the fixture JSON. create-projection-node.ts and the public API untouched.
  • Batching core (module-level queue, flushed in one microtask):
    ts
    let pendingBuilders: LayoutAnimationBuilder[] | undefined
    // constructor:
    if (!pendingBuilders) {
        pendingBuilders = []
        queueMicrotask(flushPendingBuilders)
    }
    pendingBuilders.push(this)
    
    flushPendingBuilders() mounts all nodes in document order, snapshots all builders, runs all updateDoms, reconciles additions before removals across builders (AnimatePresence-equivalent semantics per layoutId), fires one root.didUpdate(), finalizes via microtask.render.
  • Nodes persist in a module-level WeakMap<Element, IProjectionNode> reusing visualElementStore entries.
  • CI: ALL GREEN at planning time (CircleCI 17504–17508). All three Greptile threads answered; one retracted by Greptile. The animate-layout-timing React 18 Cypress failure mentioned in the body is pre-existing on clean main (author bisected).
  • Coverage gap vs #3747: #3747 has fire-and-forget.html (+ fixture JSON entry); #3748 handles fire-and-forget structurally (microtask flush runs regardless of .then()) but has no regression fixture for it.
  • No human review on any of the three big PRs; this one's Greptile threads are all resolved.

Commands you will need

PurposeCommandExpected
Checkoutgh pr checkout 3748branch animate-layout-batched-commits
Fetch 3747's fixturegit fetch origin animate-layout-shared-projection-tree && git checkout origin/animate-layout-shared-projection-tree -- dev/html/public/animate-layout/fire-and-forget.htmlfile appears
Build motion-dom only (dev loop)cd packages/motion-dom && yarn build — see memory note: animate-layout fixtures run via dev/html Vite + cypress.html.jsonexit 0
HTML E2Estart dev/html Vite server, then cd packages/framer-motion && cypress run --config-file=cypress.html.json --spec "cypress/integration-html/animate-layout*" (check exact spec path in repo; the fixture list is driven by the animate-layout JSON)all fixtures pass incl. fire-and-forget
Full CIgit push && gh pr checks 3748 --watchgreen

(Verify exact HTML-suite invocation against Makefile / package.json test-html script before running — do not guess flags.)

Scope

In scope: porting fire-and-forget.html + its JSON entry from branch animate-layout-shared-projection-tree; merging #3748; closing #3747.

Out of scope: any change to create-projection-node.ts; the public animateLayout API; reworking batching semantics; plan 009's characterization tests themselves (separate plan in plans/009-*.md).

Steps

Step 1 (GATE — maintainer decision): plan-009 sequencing

plans/README.md:31 says plan 009 (LayoutAnimationBuilder characterization tests) "should land before any refactor of LayoutAnimationBuilder.ts … it exists to make such changes deliberate." This PR is exactly such a refactor. The maintainer must either (a) execute plan 009 first, or (b) explicitly accept the 28 HTML fixtures (24 existing + 4 new) as the characterization suite. If the README row for this plan is not marked APPROVED (option b) and plan 009 is not DONE, set the row to BLOCKED ("awaiting plan-009 sequencing decision") and stop.

Step 2: Port the fire-and-forget fixture from #3747

On the #3748 branch, copy dev/html/public/animate-layout/fire-and-forget.html from branch animate-layout-shared-projection-tree (command above) and add its entry to the animate-layout fixture JSON (same file the PR already edits — match the existing entry format exactly). Memory traps for this dev loop: suppressed build output, public/ bare imports, showError re-parses body — see memory note "animate-layout dev loop" if fixtures behave oddly.

Verify: HTML E2E suite passes locally including the new fixture; commit and push; gh pr checks 3748 green.

Step 3: Merge #3748, then close #3747

gh pr merge 3748 --squash. Then gh pr close 3747 --comment "Superseded by #3748 (batched cross-builder commits; fire-and-forget fixture ported). See plans/issues/pr-3747.md." Do NOT use gh pr edit.

Verify: 3748 MERGED; 3747 CLOSED.

Step 4: Notify the downstream consumer

motion-plus (/Users/matt/Sites/plus, packages/motion-remotion and unstable_animateLayout consumers) depends on this engine. Add a line to the README row noting the rewrite landed, so the next motion-plus sync re-runs its integration tests against a motion-dom build containing this change.

Done criteria

  • Plan-009 gate resolved (009 DONE, or option-b approval recorded)
  • fire-and-forget.html + JSON entry present on the branch; HTML suite green locally
  • PR 3748 merged with all required checks green
  • PR 3747 closed with supersession comment
  • plans/issues/README.md rows for pr-3748 AND pr-3747 updated

STOP conditions

  • The HTML animate-layout suite fails on any fixture other than the known pre-existing animate-layout-timing React-18 case — report the fixture name and output; do not patch fixtures to pass.
  • Merge conflict with anything that landed since planning (e.g. if #3749 landed first — then re-run the full animate-layout HTML suite after rebase before merging; if failures appear, report).
  • The fixture JSON format has changed shape since planning.

Maintenance notes

  • The module-level pendingBuilders queue means all same-tick builders share fate; anything that makes builder construction async (e.g. awaiting before constructing) opts out of batching — document this if the public API grows.
  • PR #3749 rewrites how the projected elements render; whichever of 3748/3749 lands second must re-run the animate-layout HTML suite (enforced in pr-3749's plan).
  • memory note "animateLayout reimplementation" lists 3 gotchas (mount-before-snapshot, paused-seek interrupt, cloned-transform inflation) that reviewers of future changes to this file should know.