How I Reduced My Portfolio Bundle Size by 36KB: A Bundle Optimization Journey

2025-01-19
12 mins read
Performance
Bundle OptimizationPerformanceGSAPFramer MotionNext.js

A deep dive into analyzing and optimizing bundle size by replacing Framer Motion with GSAP, including detailed analysis, decision-making process, and measurable results.

How I Reduced My Portfolio Bundle Size by 36KB: A Bundle Optimization Journey

When building a modern web portfolio, it's easy to accumulate dependencies that bloat your bundle size. In this article, I'll walk you through my recent optimization journey where I analyzed two animation libraries and made a data-driven decision that saved 36KB (gzipped) from my client bundle.

The Problem: Too Many Animation Libraries

My portfolio was using both GSAP and Framer Motion for animations. While both are excellent libraries, having two animation frameworks for a single application is redundant and wasteful.

Initial Analysis

First, I needed to understand the actual usage and bundle impact:

GSAP Usage:

  • 7 components using GSAP
  • Total bundle: ~284 KB (uncompressed)
  • Used for complex animations: cursor tracking, scroll-triggered animations, particles, project cards

Framer Motion Usage:

  • 2 components using Framer Motion
  • Total bundle: 402 KB (stat) / 112 KB (parsed) / 36 KB (gzipped)
  • Used only for simple animations: fade-in, scale, hover effects

The Investigation

I used Next.js Bundle Analyzer to measure the exact impact:

bash
1# Enable bundle analysis
2ANALYZE=true pnpm build

This generated detailed reports showing:

1. GSAP was used in 7 critical components:

- Cursor.tsx - Custom cursor with smooth tracking

- Loader.tsx - Loading animations

- Particles.tsx - Particle system animations

- Projects.tsx - Project section scroll animations

- ProjectCard.tsx - Individual card animations

- AnimatedLink.tsx - Link hover effects

- Blog.tsx - Blog section animations

2. Framer Motion was only used in:

- SoftSkillsSection.tsx - Simple card animations

- app/admin/login/page.tsx - Login form animations

The Decision Matrix

| Factor | GSAP | Framer Motion |

| --------------------- | --------------------------------- | ------------------------ |

| Components using it | 7 | 2 |

| Bundle size (gzipped) | Already loaded | 36 KB |

| Animation complexity | High (scroll-triggers, timelines) | Low (fade, scale, hover) |

| Replaceability | Hard | Easy |

| Consistency | ✅ Primary library | ❌ Secondary library |

Verdict: Remove Framer Motion and migrate those animations to GSAP.

The Migration

Before: Framer Motion Component

tsx
1import { motion } from 'framer-motion';
2
3export const SoftSkillsSection = () => {
4  return (
5    <motion.div
6      initial={{ opacity: 0, y: 20 }}
7      animate={{ opacity: 1, y: 0 }}
8      transition={{ duration: 0.3, delay: 0.8 }}
9    >
10      {SOFT_SKILLS.map((skill, index) => (
11        <motion.div
12          key={index}
13          initial={{ opacity: 0, scale: 0.8 }}
14          animate={{ opacity: 1, scale: 1 }}
15          transition={{
16            duration: 0.3,
17            delay: index * 0.1,
18            type: 'spring',
19            stiffness: 300,
20            damping: 25,
21          }}
22          whileHover={{
23            scale: 1.05,
24            y: -5,
25          }}
26        >
27          {/* Card content */}
28        </motion.div>
29      ))}
30    </motion.div>
31  );
32};

After: GSAP Migration

tsx
1import { useRef } from 'react';
2import { gsap, useGSAP } from '@/lib/gsap';
3
4export const SoftSkillsSection = () => {
5  const containerRef = useRef<HTMLDivElement>(null);
6  const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
7
8  // Animate on mount
9  useGSAP(() => {
10    if (!containerRef.current) return;
11
12    const ctx = gsap.context(() => {
13      // Container animation
14      gsap.from(containerRef.current, {
15        opacity: 0,
16        y: 20,
17        duration: 0.3,
18        delay: 0.8,
19        ease: 'power2.out',
20      });
21
22      // Staggered cards animation
23      gsap.from(cardsRef.current.filter(Boolean), {
24        opacity: 0,
25        scale: 0.8,
26        duration: 0.3,
27        stagger: 0.1,
28        delay: 0.9,
29        ease: 'back.out(1.7)', // Spring-like easing
30      });
31    }, containerRef);
32
33    return () => ctx.revert(); // Cleanup
34  }, []);
35
36  // Hover effects
37  const handleCardMouseEnter = (index: number) => {
38    const card = cardsRef.current[index];
39    if (!card) return;
40
41    gsap.to(card, {
42      scale: 1.05,
43      y: -5,
44      duration: 0.2,
45      ease: 'power2.out',
46    });
47  };
48
49  const handleCardMouseLeave = (index: number) => {
50    const card = cardsRef.current[index];
51    if (!card) return;
52
53    gsap.to(card, {
54      scale: 1,
55      y: 0,
56      duration: 0.2,
57      ease: 'power2.out',
58    });
59  };
60
61  return (
62    <div ref={containerRef}>
63      {SOFT_SKILLS.map((skill, index) => (
64        <div
65          key={index}
66          ref={(el) => {
67            cardsRef.current[index] = el;
68          }}
69          onMouseEnter={() => handleCardMouseEnter(index)}
70          onMouseLeave={() => handleCardMouseLeave(index)}
71        >
72          {/* Card content */}
73        </div>
74      ))}
75    </div>
76  );
77};

Key Migration Patterns

1. Converting Declarative to Imperative

Framer Motion uses declarative animations:

tsx
1<motion.div initial={...} animate={...} />

GSAP uses imperative animations:

tsx
1gsap.from(element, { ...properties });

2. Handling Stagger Animations

Framer Motion:

tsx
1transition={{ delay: index * 0.1 }}

GSAP:

tsx
1gsap.from(elements, { stagger: 0.1 });

3. Spring Physics to Easing Functions

Framer Motion:

tsx
1transition={{ type: 'spring', stiffness: 300, damping: 25 }}

GSAP:

tsx
1ease: 'back.out(1.7)'; // Approximates spring behavior

4. Cleanup

Framer Motion: Automatic cleanup

GSAP with useGSAP:

tsx
1useGSAP(() => {
2  const ctx = gsap.context(() => {
3    // animations
4  });
5  return () => ctx.revert(); // Manual cleanup
6}, []);

Removal Process

After migration, I removed all Framer Motion dependencies:

bash
1# Remove from package.json
2pnpm remove framer-motion
3
4# Update Next.js config
5# Before:
6optimizePackageImports: ['lucide-react', 'framer-motion']
7
8# After:
9optimizePackageImports: ['lucide-react']

Results

Bundle Size Comparison

Before:

  • Total client bundle: Included Framer Motion (36 KB gzipped)
  • Shared chunks: 87.6 KB + Framer Motion

After:

  • Total client bundle: No Framer Motion
  • Shared chunks: 87.6 KB
  • Net savings: 36 KB (gzipped)

Verification

I verified the removal by:

1. Building with bundle analyzer:

bash
1ANALYZE=true pnpm build

2. Checking for Framer Motion in chunks:

bash
1find .next/static/chunks -name "*.js" | xargs grep -l "framer-motion" | wc -l
2# Result: 0 ✅

3. Testing all animations still worked correctly

Lessons Learned

1. Measure Before Optimizing

Don't assume which library is heavier. Use tools like Bundle Analyzer to get real data.

2. Consider Usage Scope

A library used in 1-2 components is a prime candidate for removal, especially if the functionality can be replaced.

3. Migration ROI

36 KB might seem small, but:

  • It's downloaded and parsed on every page load
  • It adds to the main bundle blocking initial render
  • Mobile users on slow connections notice the difference

4. Consistency is a Feature

Having a single animation library means:

  • Easier onboarding for new developers
  • Consistent animation behavior
  • Single source of truth for debugging

Performance Impact

Using WebPageTest and Lighthouse:

Before:

  • First Load JS: 226 KB (homepage)
  • Bundle parse time: ~45ms (on 4x CPU slowdown)

After:

  • First Load JS: 226 KB (homepage) - GSAP was already loaded
  • Bundle parse time: ~42ms (on 4x CPU slowdown)
  • 3ms faster due to less code to parse

While the GSAP library itself is still in the bundle, removing Framer Motion means:

  • One less library to parse and execute
  • Fewer dependencies to tree-shake
  • Cleaner module graph

Conclusion

This optimization journey taught me that every dependency should justify its existence. When I found that Framer Motion was only used in 2 components for simple animations that could easily be replicated with GSAP (which was already loaded), the decision became clear.

Final Checklist for Bundle Optimization

  • [ ] Analyze bundle with @next/bundle-analyzer
  • [ ] Identify duplicate functionality across libraries
  • [ ] Measure actual usage (number of components)
  • [ ] Assess migration effort vs. savings
  • [ ] Migrate and test thoroughly
  • [ ] Verify removal in production bundle
  • [ ] Measure performance impact

Key Takeaway: Don't accept dependencies blindly. Regularly audit your bundle, question each library's necessity, and be willing to refactor for consistency and performance.

---

_This optimization was part of improving my portfolio from an 88/100 to 95+/100 score. Check out my other posts on testing strategy and accessibility improvements!_

About the Author Performance

Experienced software engineer passionate about creating innovative solutions and sharing knowledge through technical writing.

More from the Blog

Explore more articles and insights on software engineering, technology, and career development.

View all articles

Share this article

Help others discover this article by sharing it

Article Info

Published2025-01-19
Read Time12 mins read
AuthorPerformance

Stay Updated

Follow me on Medium to get notified about new articles and insights.

Follow on Medium