Skip to content

perf(motion): lazy-load framer-motion so non-animated routes pay zero… - #706

Merged
Calebux merged 4 commits into
zintarh:mainfrom
AnnieIj:perf/651-lazy-motion-v2
Oct 1, 2026
Merged

Calebux merged 4 commits into
zintarh:mainfrom
AnnieIj:perf/651-lazy-motion-v2

Conversation

@AnnieIj

@AnnieIj AnnieIj commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Solution

  1. Consolidate to one library Replace all 7 motion/react imports with ramer-motion. Both packages are the same code; one canonical import makes the bundle-analyser measurement meaningful and avoids duplicate chunks.

  2. LazyMotion provider (app/components/MotionProvider.tsx) Wraps the locale layout in framer-motion's LazyMotion, which moves the domAnimation feature-set (~50 kB) into a separate async chunk that is fetched on demand rather than inlined into every route.

    Reduced-motion shortcut: when prefers-reduced-motion is set the features loader is replaced with () => Promise.resolve({}), so the animation chunk is never fetched at all for those users.

  3. Central re-export (app/utils/motionExports.ts) Re-exports m and AnimatePresence from framer-motion so components that want to participate in LazyMotion import from one place.

  4. Switch 5 route pages to m.* API withdraw, governance, export-csv, offers, loading These pages used full motion.* components. Switching to m.* means the feature bundle is shared via LazyMotion rather than each page independently pulling it in.

  5. optimizePackageImports in next.config.ts Tells Next.js to tree-shake framer-motion per chunk so only the symbols actually imported end up in each route's JS.

Effect

  • Routes that import no animated components (ledgers, recent-ledgers) never touch the animation runtime.
  • Routes that do animate share a single async domAnimation chunk loaded once per session, not once per route.
  • Users with prefers-reduced-motion: reduce never download the chunk.

Tests

9/9 MotionProvider unit tests pass:
MOTION_FEATURES_LOADER (2 tests) reduced-motion skip path (3 tests) getPrefersReducedMotion (4 tests)

Closes #651

@vercel

vercel Bot commented Sep 27, 2026

Copy link
Copy Markdown

@zintarh is attempting to deploy a commit to the zintarh Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Sep 27, 2026

Copy link
Copy Markdown

@AnnieIj Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

… cost

Problem
-------
The app shipped two animation libraries (framer-motion and motion/react).
The full animation runtime loaded on every route regardless of whether
the route used any animation at all (zintarh#651).

Changes
-------
1. Consolidate to one library
   Replace motion/react imports in ShareCard.tsx and ShareButtons.tsx
   with framer-motion. Both packages are the same code at v12; one
   canonical import makes bundle-analyser output meaningful.

2. LazyMotion provider (app/components/MotionProvider.tsx)
   Wraps children in framer-motion's LazyMotion, moving the domAnimation
   feature-set (~50 kB) into a separate async chunk fetched on demand.

   Reduced-motion shortcut: when prefers-reduced-motion is set the
   features loader returns Promise.resolve({}) so the animation chunk
   is never fetched at all for those users.

3. Central re-export (app/utils/motionExports.ts)
   Re-exports m and AnimatePresence from framer-motion so components
   participating in LazyMotion import from one place.

4. useReducedMotion hook (app/hooks/useReducedMotion.ts)
   Pure, SSR-safe hook that tracks prefers-reduced-motion. Drives the
   skip-loading decision in MotionProvider.

5. optimizePackageImports in next.config.ts
   Tells Next.js to tree-shake framer-motion per chunk.

Effect
------
- Routes importing no animated components pay zero animation runtime cost.
- Animated routes share a single async domAnimation chunk loaded once
  per session, not once per route.
- Users with prefers-reduced-motion: reduce never download the chunk.

Tests
-----
9/9 MotionProvider unit tests pass.

Closes zintarh#651
@AnnieIj
AnnieIj force-pushed the perf/651-lazy-motion-v2 branch from a26965d to a370084 Compare September 27, 2026 23:07
@Calebux
Calebux merged commit 5b8a211 into zintarh:main Oct 1, 2026
3 of 15 checks passed
@Calebux

Calebux commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the contribution! This PR conflicted with recent changes on main, so the conflicts were resolved during merge:

  • next.config.ts: I combined the target branch's next-intl/bundle-analyzer/header/image-format config with this PR's experimental optimizePackageImports and image remotePatterns; no intended changes were dropped.

Resolved and merged with PRmaster. Your commits are included in the merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: framer-motion and motion animations are loaded on every route

3 participants