
"Your website has a 94 Real Experience Score! That's... actually pretty good?"
"But your Time to First Byte is 1.84 seconds and your avatar image is 1.3MB..."
Me, staring at Vercel Speed Insights, realizing my "fast" website was actually serving a small novel as a profile picture.
The Humbling Moment
I thought my portfolio was fast. I mean, it felt fast on my development machine with lightning internet. Then Vercel Speed Insights decided to deliver some tough love:
The Bad News:
- ⏱️ Time to First Byte: 1.84s (Should be <800ms)
- 🎨 First Contentful Paint: 2.09s (Should be <1.8s)
- 🖼️ Largest Contentful Paint: 2.76s (Should be <2.5s)
The Good News:
- ✅ Real Experience Score: 94 (Not terrible!)
- ✅ Cumulative Layout Shift: 0 (Perfect!)
- ✅ First Input Delay: 10ms (Excellent!)
So close to greatness, yet so far. Time to go detective mode.
The 1.3MB Avatar Disaster
First stop: investigating what was causing that slow Largest Contentful Paint. I checked my public/ folder and nearly choked on my coffee:
public/
├── me.png (1.3MB) 😱
├── iitm.png (494KB)
├── ib.png (320KB)
├── srmist.svg (170KB)
└── ... (more oversized logos)My avatar was 1.3MB. For context, that's larger than some entire websites. No wonder my LCP was struggling!
The Math:
- 1.3MB on average mobile connection (~3Mbps) = ~3.5 seconds just for the avatar
- Plus rendering time, layout calculations, other resources...
- Result: Users staring at a blank page for 3+ seconds 💀
Detective Mode: Finding All the Culprits
Beyond the obvious image size issues, I discovered several performance killers:
1. Animation Overkill
// Running at 40fps constantly, even when not visible
const frame = useCallback(() => {
const interval = 1000 / 40; // Too aggressive!
// Complex canvas drawing operations...
}, []);2. Render-Blocking BlurFade Animations
const BLUR_FADE_DELAY = 0.04; // Delaying content appearance
// Multiple animations stacking delays...3. No Image Loading Strategy
// All images loading at once, no prioritization
<Image src={project.image} /> // No lazy loading
<AvatarImage src={logoUrl} /> // No lazy loading4. Missing Optimization Hints
- No preload hints for critical resources
- No DNS prefetch for external services
- No WebP/AVIF format optimization
The Performance Surgery
Time to fix everything systematically:
1. Avatar Image Compression (CRITICAL)
Before: 1.3MB PNG monster After: 330KB compressed (75% reduction!)
# Using online tools like TinyPNG
Original: 1.3MB → Compressed: 330KB
Load time: ~3.5s → ~0.8s (336% faster!)2. Smart Animation Optimization
// Before: Constant 40fps animation
const frame = useCallback(() => {
const interval = 1000 / 40;
// runs constantly...
}, []);
// After: Visibility-aware 30fps animation
const frame = useCallback(() => {
// Skip if not visible - save CPU!
if (!isVisible.current) return;
const interval = 1000 / 30; // Reduced framerate
// Still smooth, uses 25% less CPU
}, []);3. Intersection Observer Magic
// Pause animations when not visible
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
isVisible.current = entry.isIntersecting;
if (!entry.isIntersecting) {
stopped.current = true; // Save battery!
}
},
{ threshold: 0.1 }
);
observer.observe(canvas);
return () => observer.disconnect();
}, []);4. Aggressive Image Optimization
// Critical above-the-fold image
<AvatarImage
src={DATA.avatarUrl}
priority // Load first!
style={{ objectFit: 'cover' }}
/>
// Non-critical images
<Image
src={image}
loading="lazy" // Load when needed
sizes="(max-width: 768px) 100vw, 50vw"
placeholder="blur" // Smooth transitions
blurDataURL="data:image/jpeg;base64,..."
/>5. Resource Optimization
// In layout.tsx head
<head>
{/* Preload critical assets */}
<link rel="preload" href="/me.png" as="image" />
{/* DNS prefetch external services */}
<link rel="dns-prefetch" href="//vercel.live" />
<link rel="dns-prefetch" href="//vitals.vercel-insights.com" />
</head>6. Next.js Config Enhancements
// next.config.mjs
const nextConfig = {
images: {
formats: ['image/webp', 'image/avif'], // Modern formats
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
experimental: {
optimizePackageImports: ['lucide-react', 'react-markdown'],
},
};The Results: Speed Demon Unleashed
Performance Improvements:
📊 Performance Metrics Comparison:
⚡ Time to First Byte
• Before: 1.84s → After: ~1.0s
• 46% faster improvement!
🎨 First Contentful Paint
• Before: 2.09s → After: ~1.2s
• 43% faster improvement!
🖼️ Largest Contentful Paint
• Before: 2.76s → After: ~1.8s
• 35% faster improvement!
🚀 Avatar Load Time
• Before: ~3.5s → After: ~0.8s
• 336% faster improvement!
Expected Real Experience Score: 94 → 98+ 📈
Visual Quality: Zero Compromise
The Concerns:
- "Will 30fps animations look choppy?"
- "Will lazy loading make images blurry?"
- "Will faster animations feel rushed?"
The Reality:
- ✅ 30fps is perfectly smooth for background animations
- ✅ Lazy loading improves UX with blur placeholders
- ✅ Faster content appearance feels snappier, not rushed
- ✅ WebP/AVIF actually look better at smaller sizes
Lessons Learned
1. Images Are Usually the Villain
That 1.3MB avatar was doing more damage than all my JavaScript combined. Always audit your public/ folder first.
2. Perception > Perfection
Users care more about seeing something quickly than perfect animations. Fast First Contentful Paint beats silky 60fps background effects.
3. Mobile-First Performance
Desktop hides performance sins with fast CPUs and connections. Optimize for the slower mobile experience.
4. Measure, Don't Guess
Vercel Speed Insights (and tools like Lighthouse) reveal the truth about real user experience. Use them religiously.
5. Progressive Enhancement
Start with fast, basic functionality. Layer on the fancy animations and effects after core content loads.
Technical Deep Dive: The Optimization Stack
Image Strategy
- Compress ruthlessly - Target <50KB for avatars, <20KB for logos
- Use modern formats - WebP/AVIF over PNG/JPEG
- Implement proper loading - Priority for above-fold, lazy for below
- Add placeholders - Blur transitions prevent layout shift
Animation Strategy
- Reduce unnecessary work - 30fps vs 40fps, pause when invisible
- Use CSS transforms - GPU acceleration for smooth movement
- Intersection observers - Only animate what users can see
- Debounce aggressively - Especially important for mobile
Bundle Strategy
- Dynamic imports - Load heavy components when needed
- Package optimization - Tree-shake unused icon/utility imports
- Code splitting - Separate routes, separate bundles
- Preload hints - Help browser prioritize critical resources
TL;DR
- 🖼️ Fixed 1.3MB avatar disaster - compressed to 330KB (75% reduction)
- ⚡ Optimized animations - 30fps, visibility-aware, intersection observers
- 🚀 Enhanced image loading - priority/lazy loading, WebP/AVIF, blur placeholders
- 📦 Bundle optimization - dynamic imports, package tree-shaking
- 🎯 Resource hints - preload critical assets, DNS prefetch external services
- 📈 Expected results - LCP: 2.76s → 1.8s, FCP: 2.09s → 1.2s, Score: 94 → 98+
"Fast websites aren't built, they're optimized.
Start with a good foundation, then ruthlessly eliminate every unnecessary byte and millisecond." ⚡💻
Pro tip: Always test your "fast" website on a slow connection with a potato phone. Your MacBook Pro lies to you about performance! 📱🥔