The GPU-safe properties rule
The single most important performance decision in any animation is which CSS properties you animate. Only two properties trigger GPU compositing without causing layout recalculation: transform (including translate, scale, rotate) and opacity. Animating anything else — width, height, top, left, padding, margin, border-radius during an animation — forces the browser to run layout and paint every frame, which destroys frame rates on mid-tier devices. In Framer Motion, this means preferring x, y, scaleX, scaleY, and opacity in your animate prop. If you need a card to feel like it is growing, use scaleX/scaleY from the transform origin, not animating width. The visual result is identical. The performance difference is significant.
useInView instead of whileInView for complex sequences
Framer Motion's whileInView prop is elegant for simple single-element reveals but becomes limiting when you need staggered children, conditional animation, or access to the inView boolean in logic. I use the useInView hook from framer-motion for anything beyond a single element. Pair it with { once: true, amount: 0.3 } for most scroll reveals — this means the animation triggers when 30% of the element is visible and never replays. For hero sections and above-the-fold elements, skip inView entirely and animate on mount with a short delay to avoid the 'nothing visible' flash on fast loads.
Spring config discipline
Default Framer Motion springs feel bouncy and toy-like on professional interfaces. For B2B product and portfolio work, I use two spring presets: a tight spring for UI feedback (stiffness 320, damping 28, mass 0.38 — for buttons, cards, interactive states) and a soft spring for enter/exit transitions (stiffness 100, damping 20, mass 0.5 — for modals, panels, page elements). Never use bounce in professional contexts. Mass is the most underused property — increasing it slightly adds physical weight that makes motion feel more real. Decreasing it makes elements feel lighter and more responsive. Test with real content, not lorem ipsum; text-heavy components need different spring tuning than icon animations.
Avoiding bundle weight on non-animated pages
Framer Motion adds roughly 60kb to your bundle (gzipped, tree-shaken). On a portfolio or marketing site where only a few pages have heavy animation, this weight should not be paid globally. In Next.js App Router, I keep motion components in separate client components and import them only where needed. The layout.tsx never imports Framer Motion directly — it mounts wrappers like CustomCursor and PageLoadIntro as isolated components. Blog post pages and simple content pages get only the lightweight Reveal component, not the full interactive card set. Profile pages in Chrome DevTools at page load; if Framer Motion chunks appear on a static page, an import is leaking through a shared barrel file.