Environment
- next
16.2.7 — production build (Turbopack); dev server is unaffected
- motion
12.40.0 (imported as motion/react)
- motion-plus
npm:@motionplus/core@2.12.0
- 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 getOrCreateRecord → visualElementStore.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
- 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.
- Or document a required bundler alias (e.g. Turbopack
resolveAlias) for motion-plus consumers.
- 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).
Environment
16.2.7— production build (Turbopack); dev server is unaffected12.40.0(imported asmotion/react)npm:@motionplus/core@2.12.019.2.6Summary
unstable_animateLayoutworks 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 frommotion/react. In our production chunk graph that resolves to a granular module carrying onevisualElementStoreWeakMap.@motionplus/core'sanimate-layout.mjsimportsparseAnimateLayoutArgs/LayoutAnimationBuilderfrom'motion'(the barrel). In the same production build, Turbopack emitted a second, concatenated barrel module containing its own copy ofvisualElementStore.So
m.trrows register in store A, whileLayoutAnimationBuilder'sgetOrCreateRecord→visualElementStore.get(element)reads store B. Every lookup misses, each element takes the create-own-visual-element path, and the FLIP animates nothing — whilemeasure()numbers are all correct.Dev is fine because the dev server unifies the module graph (single store instance).
Evidence
visualElementStoreWeakMap declarations: one in the granularmotion/reactmodule, one inside a concatenated barrel module reached only via motion-plus'sfrom 'motion'imports.For what it's worth,
animate-activity.mjsimportsPresenceChildfrom'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
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.resolveAlias) for motion-plus consumers.visualElementStorelookups systematically miss for elements that carry motion props — this failure is currently 100% silent.Workaround we shipped
Replaced the
unstable_animateLayoutwrapper 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/reactcomponents plusunstable_animateLayout).