#3772·motion

unstable_animateLayout silently no-ops in Turbopack production builds — two visualElementStore instances (motion-plus imports the 'motion' barrel)

Author: f01xyzCreated Jul 26, 2026Updated Jul 26, 2026

Environment

  • next 16.2.7production build (Turbopack); dev server is unaffected
  • motion 12.40.0 (imported as motion/react)
  • motion-plus npm:@motionplus/[email protected]
  • react / react-dom 19.2.6
  • pnpm monorepo

Summary

unstable_animateLayout works in dev and silently no-ops in a Turbopack production build — it measures correct before/after rects, then animates zero elements. No warning or error is emitted, so every dev-side verification passes while production ships a dead animation.

Root cause (from chunk-graph analysis)

The app registers elements through m.* components imported from motion/react. In our production chunk graph that resolves to a granular module carrying one visualElementStore WeakMap.

@motionplus/core's animate-layout.mjs imports parseAnimateLayoutArgs / LayoutAnimationBuilder from 'motion' (the barrel). In the same production build, Turbopack emitted a second, concatenated barrel module containing its own copy of visualElementStore.

So m.tr rows register in store A, while LayoutAnimationBuilder's getOrCreateRecordvisualElementStore.get(element) reads store B. Every lookup misses, each element takes the create-own-visual-element path, and the FLIP animates nothing — while measure() numbers are all correct.

Dev is fine because the dev server unifies the module graph (single store instance).

Evidence

  • Live instrumentation on the prod build: the builder measured 33/66/99 px row deltas and then animated 0 elements (animation timeline showed transforms at 0 within ~10 ms).
  • Parsing the emitted chunks shows two distinct visualElementStore WeakMap declarations: one in the granular motion/react module, one inside a concatenated barrel module reached only via motion-plus's from 'motion' imports.
  • Same page, same interaction, dev build: FLIP animates correctly.

For what it's worth, animate-activity.mjs imports PresenceChild from 'motion/react' (granular) and does not exhibit the problem — the fault line is specifically the 'motion' barrel import inside store-dependent entry points.

Suggested directions

  1. Import granular specifiers (motion/react / motion-dom) inside motion-plus's store-dependent modules so app code and motion-plus converge on one module instance under bundler chunking.
  2. Or document a required bundler alias (e.g. Turbopack resolveAlias) for motion-plus consumers.
  3. Or emit a dev/prod warning when visualElementStore lookups systematically miss for elements that carry motion props — this failure is currently 100% silent.

Workaround we shipped

Replaced the unstable_animateLayout wrapper with a self-contained WAAPI FLIP (measure → flushSync → measure → el.animate), which is bundler-independent.

Happy to provide the chunk excerpts or a minimal repro (Next 16 + Turbopack prod build, one page importing motion/react components plus unstable_animateLayout).