How I Reduced My Portfolio Bundle Size by 36KB: A Bundle Optimization Journey
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:
1# Enable bundle analysis
2ANALYZE=true pnpm buildThis 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
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
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:
1<motion.div initial={...} animate={...} />GSAP uses imperative animations:
1gsap.from(element, { ...properties });2. Handling Stagger Animations
Framer Motion:
1transition={{ delay: index * 0.1 }}GSAP:
1gsap.from(elements, { stagger: 0.1 });3. Spring Physics to Easing Functions
Framer Motion:
1transition={{ type: 'spring', stiffness: 300, damping: 25 }}GSAP:
1ease: 'back.out(1.7)'; // Approximates spring behavior4. Cleanup
Framer Motion: Automatic cleanup
GSAP with useGSAP:
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:
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:
1ANALYZE=true pnpm build2. Checking for Framer Motion in chunks:
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!_
More from the Blog
Explore more articles and insights on software engineering, technology, and career development.
View all articles