motiondivision / motiondivision/motion
unstable_animateLayout silently no-ops in Turbopack production builds — two visualElementStore instances (motion-plus imports the 'motion' barrel)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 33.7k
- Forks
- 1.4k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 14
Description
Environment
- next
16.2.7— production build (Turbopack); dev server is unaffected - motion
12.40.0(imported asmotion/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
visualElementStoreWeakMap declarations: one in the granularmotion/reactmodule, one inside a concatenated barrel module reached only via motion-plus'sfrom '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
visualElementStorelookups 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).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with @motionplus/core's animate-layout.mjs and compare its 'motion' imports with the motion/react imports used by the app. Reproduce the Next 16.2.7 Turbopack production build, inspect emitted chunks for duplicate visualElementStore declarations, and compare with animate-activity.mjs. Done means unstable_animateLayout finds the registered elements and animates them in production without breaking the working dev path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- build-system, frontend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100