Skip to content

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

Description

@f01xyz

Environment

  • next 16.2.7production 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 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions