Skip to content

perf(motion): lazy-load framer-motion via LazyMotion + m (~12KB gzip/component) - #334

Open
DipeshRajoria007 wants to merge 1 commit into
build/tsup-tree-shakingfrom
perf/framer-lazymotion
Open

perf(motion): lazy-load framer-motion via LazyMotion + m (~12KB gzip/component)#334
DipeshRajoria007 wants to merge 1 commit into
build/tsup-tree-shakingfrom
perf/framer-lazymotion

Conversation

@DipeshRajoria007

Copy link
Copy Markdown
Contributor

What

Lazy-load framer-motion features in grauity's animated components via LazyMotion + the lightweight m component, instead of importing the full eager motion.* API.

A new shared provider common/motion.tsx:

import { domAnimation, LazyMotion } from 'framer-motion';

const GrauityLazyMotion = ({ children }) => (
    <LazyMotion features={domAnimation}>{children}</LazyMotion>
);

The 7 animated components — Modal, Drawer, BottomSheet, Overlay, PopOver, Accordion, DropdownMenu — now:

  • wrap their render tree in <GrauityLazyMotion>,
  • use styled(m.div) instead of styled(motion.div) (*.styles.ts),
  • use m.* instead of inline motion.* (Accordion).

Why (grauity perf)

domAnimation bundles initial / animate / exit / variants / transition — every animation these components actually use — while excluding the drag / layout / pan / gesture features that eager motion.* always pulls in. grauity uses none of those excluded features.

Measured impact

Bundling each animated component on its own (esbuild, minified, react/react-dom external), before (motion) vs after (m + LazyMotion):

Component gzip before → after raw before → after
NSModal 87.6 → 75.6 KB (−12.0) 278.7 → 237.4 KB (−41.3)
NSDrawer 86.1 → 74.1 KB (−12.0) 272.1 → 230.8 KB (−41.3)
NSBottomSheet 86.4 → 74.3 KB (−12.1) 272.9 → 231.6 KB
NSAccordion 86.2 → 74.2 KB (−12.0) 272.6 → 231.3 KB
NSPopOver 85.8 → 73.7 KB (−12.1) 270.7 → 229.4 KB
NSOverlay 85.8 → 73.7 KB (−12.1) 270.7 → 229.4 KB
NSDropdownMenu 85.8 → 73.7 KB (−12.1) 270.7 → 229.4 KB

~12 KB gzip (~41 KB raw, ~14%) off every animated component, no public API or runtime-behaviour change.

No public API change

Component props, exports, and animation behaviour are unchanged. This builds on the tree-shakeable tsup build (base branch build/tsup-tree-shaking), so consumers that don't import an animated component pay nothing, and those that do now ship less framer-motion.

Testing — no regression

  • tsc --noEmit: ✅ clean
  • eslint: ✅ clean
  • prettier --check: ✅ clean
  • jest: ✅ 46 suites / 412 tests pass
  • tsup build-lib: ✅ builds, types + dts shim emitted
  • Real-browser (Vite + Playwright, real framer-motion under LazyMotion):
    • Modal opens (enter animation, m.div + LazyMotion) — ✅ PASS
    • Modal closes: #modal-body present mid-exit, then unmounted — ✅ PASS (this is the critical check — confirms AnimatePresence exit animation works under LazyMotion/domAnimation)
    • Accordion expands (m.div) — ✅ PASS
    • Zero framer-motion / LazyMotion console errors (a missing-feature misconfiguration would log one)

Files

  • common/motion.tsx — new shared LazyMotion(domAnimation) provider
  • ui/elements/{Modal,Drawer,BottomSheet,Overlay,PopOver,Accordion,DropdownMenu}/* — wrap return in <GrauityLazyMotion>; motion.*m.*
  • __mocks__/framer-motion.tsx — add m / LazyMotion / domAnimation / domMax to the test mock

🤖 Generated with Claude Code

Wrap the animated components (Modal, Drawer, BottomSheet, Overlay,
PopOver, Accordion, DropdownMenu) in a shared <GrauityLazyMotion>
provider and swap eager `motion.*` for the lightweight `m.*` component
with the `domAnimation` feature bundle.

`domAnimation` covers initial/animate/exit/variants/transition (every
animation these components use) while excluding the drag/layout/pan/
gesture features that eager `motion.*` always bundles. No public API or
runtime-behaviour change.

Per animated component this trims ~12 KB gzip (~41 KB raw, ~14% of the
component's bundle) of unused framer-motion feature code.

- common/motion.tsx: shared LazyMotion(domAnimation) provider
- *.styles.ts: styled(motion.div) -> styled(m.div)
- *.tsx: wrap component return in <GrauityLazyMotion>; Accordion inline
  motion.div -> m.div
- __mocks__/framer-motion.tsx: add m / LazyMotion / domAnimation / domMax

Verified: tsc, eslint, prettier, jest (46 suites / 412 tests), tsup
build, and a real-browser Playwright run (modal enter, AnimatePresence
exit under LazyMotion, accordion expand) with zero framer-motion /
LazyMotion console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MihirSachdeva added a commit that referenced this pull request Jun 16, 2026
…SSR fix (#336)

## Summary
Consolidated **performance** release of grauity, cut as
**`3.4.0-beta.2`** and validated against real consumer apps. This is the
integration branch combining the four perf PRs **plus** the fixes found
during beta testing, merged up to date with `master`.

## What's in it

**Perf suite (PRs #332#335):**
- **#332** — static token CSS + Tailwind `@theme` export; drops runtime
`createGlobalStyle`/`extractColorVariables` injection (`./tokens.css`,
`./theme.css`).
- **#333** — tree-shakeable per-module **tsup** build (replaces Parcel)
+ `exports` map + precise `sideEffects` + code-splitting.
- **#334** — framer-motion **`LazyMotion`** + `m` + `domAnimation` (~12
KB gz/animated component).
- **#335** — **lodash → lodash-es** (tree-shaken) + inline Icon utils
(~68 KB raw / ~13 KB gz off dist).

**Fixes found during beta testing (the reason for beta.1/beta.2):**
- **SSR crash fix** — switched all 46 source files from `import styled`
(default) to the **named** `import { styled }`. styled-components v6's
*default* export is `undefined` under Node's native ESM loader (which
Next.js uses during "Collecting page data"), so the previous beta
crashed consumer builds (`styled.i.attrs` of undefined). The named
import works under Node-ESM, CJS, and bundlers — no `node`-condition
workaround needed.
- **Barrel tree-shaking fix** — tsup entry glob `index.ts` →
`index.{ts,tsx}`, so the 9 `.tsx` components (PopOver, SelectDropdown,
Tooltip, DropdownMenu, RangeInput, FloatingActionButton,
Form/{Dropdown,FormRow,TextArea}) get their own chunks instead of being
inlined into the barrel. Result: `import { NSButton } from
'@newtonschool/grauity'` drops framer-motion + yup — **71.5 KB → 14.3 KB
gz** — so barrel consumers get the tree-shaking win with **no import
changes**.

**Merged up to `master`:** includes the transient-props
styled-components refactor (#327), `3.3.13`, and the new icons (#330,
#331). The `Modal.{tsx,styles.ts}` conflicts were resolved by keeping
master's transient-props refactor **and** the LazyMotion/named-import
changes.

**Build fix:** added the `@rollup/rollup-linux-x64-gnu` install entry to
`package-lock.json` (the lock had been regenerated on macOS, dropping
the Linux rollup binary → the publish CI's `npm ci` failed on `tsup`;
npm/cli#4828).

## Verification (no regressions)
- `tsc --noEmit`, `eslint`, `prettier --check`, **`jest` 46 suites / 412
tests**, `tsup` build — all green on the merged tree.
- Node native-ESM `import()` of the barrel + subpaths resolves (SSR
crash gone); browser tree-shaking preserved.
- **Real consumers on `3.4.0-beta.1`:** newton-web (`next build` —
258/258 pages, the page that crashed on beta.0 now builds) and
newton-school-public-website — **both CI green**.

## Publishing notes
- Publish under the **`beta`** dist-tag — the workflow's `npm-publish`
defaults to `latest`, which would clobber `3.3.13` for all consumers.
After publishing, run `npm dist-tag add @newtonschool/grauity@3.3.13
latest` to restore stable.
- Source PRs #332#335 remain open; this PR is the consolidated beta.
The team decides whether to merge this combined PR or land them
individually for the stable `3.4.0`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Mihir Sachdeva <mihirsachdeva.ms@gmail.com>
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.

1 participant