Optimizing React Performance: Lessons from Building a High-Traffic Portfolio
Optimizing React Performance: Lessons from Building a High-Traffic Portfolio
When I first deployed my portfolio website built with Next.js and React, I was excited to share my work with the world. However, I quickly realized that performance matters just as much as functionality. In this post, I'll share the optimization strategies that helped me achieve a 95+ Lighthouse performance score and reduce my initial load time to under 1.5 seconds.
The Performance Problem
My initial portfolio had several issues:
- Initial load time: 4.2 seconds on 3G
- First Contentful Paint (FCP): 2.8 seconds
- Time to Interactive (TTI): 5.1 seconds
- Bundle size: 487 KB (uncompressed)
These metrics were unacceptable for a professional portfolio that needed to make a great first impression.
Strategy 1: Code Splitting and Lazy Loading
The first major improvement came from implementing proper code splitting. Instead of loading all components upfront, I used dynamic imports for components that weren't immediately needed.
Before:
```jsx import CommandPalette from './components/CommandPalette' import ImageLightbox from './components/ImageLightbox' import GeminiChat from './components/GeminiChat' ```
After:
```jsx import dynamic from 'next/dynamic'
const CommandPalette = dynamic(() => import('./components/CommandPalette'), { ssr: false })
const ImageLightbox = dynamic(() => import('./components/ImageLightbox'), { ssr: false })
const GeminiChat = dynamic(() => import('./components/GeminiChat')) ```
Impact: Reduced initial bundle size by 42% (487 KB → 282 KB)
Strategy 2: Image Optimization
Images are often the biggest performance bottleneck. I implemented several optimizations:
1. Next.js Image Component
Switched from <img> tags to Next.js <Image> component for automatic optimization:
```jsx import Image from 'next/image'
```
2. Modern Image Formats
Configured Next.js to serve WebP and AVIF formats with automatic fallbacks:
```js // next.config.js module.exports = { images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384] } } ```
Impact: Reduced image payload by 67% (1.2 MB → 400 KB)
Strategy 3: Memoization and useMemo
For expensive computations, I used React's memoization hooks to prevent unnecessary re-renders:
```jsx import { useMemo } from 'react'
function ProjectList({ projects, filter }) { const filteredProjects = useMemo(() => { return projects.filter(project => project.skills.some(skill => skill.toLowerCase().includes(filter.toLowerCase()) ) ).sort((a, b) => new Date(b.date) - new Date(a.date)) }, [projects, filter])
return
Strategy 4: Virtualization for Long Lists
When displaying lists of projects or blog posts, I implemented virtual scrolling to only render visible items:
```jsx import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualizedList({ items }) { const parentRef = useRef(null)
const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5 })
return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px` }}> {virtualizer.getVirtualItems().map(virtualItem => ( <div key={virtualItem.index} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)` }} > {items[virtualItem.index]} ))} ) } ```
Impact: Can now handle 10,000+ items with smooth 60fps scrolling
Strategy 5: Font Optimization
I switched from Google Fonts to local fonts using Next.js font optimization:
```jsx import localFont from 'next/font/local'
const geistSans = localFont({ src: './fonts/GeistVF.woff', variable: '--font-geist-sans', weight: '100 900', display: 'swap' }) ```
Impact: Eliminated render-blocking external font requests, improved FCP by 0.8s
Strategy 6: Server-Side Rendering (SSR) and Static Generation
Leveraged Next.js's built-in rendering capabilities:
- Static Generation for blog posts (built at compile time)
- Server-Side Rendering for dynamic content (generated per request)
- Incremental Static Regeneration for content that updates occasionally
```jsx // Blog post page with ISR export async function generateStaticParams() { const posts = await getAllBlogPosts() return posts.map(post => ({ slug: post.slug })) }
export const revalidate = 3600 // Revalidate every hour ```
Strategy 7: Bundle Analysis and Tree Shaking
I used webpack bundle analyzer to identify and eliminate unnecessary dependencies:
```bash npm install @next/bundle-analyzer ```
```js // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' })
module.exports = withBundleAnalyzer({ // ... config }) ```
Findings:
- Removed unused Lodash imports (saved 72 KB)
- Replaced moment.js with date-fns (saved 67 KB)
- Removed duplicate dependencies
Final Results
After implementing these optimizations:
| Metric | Before | After | Improvement | |--------|--------|-------|-------------| | Initial Load Time | 4.2s | 1.3s | 69% faster | | First Contentful Paint | 2.8s | 0.9s | 68% faster | | Time to Interactive | 5.1s | 1.8s | 65% faster | | Bundle Size | 487 KB | 282 KB | 42% smaller | | Lighthouse Score | 67 | 96 | +29 points | | Image Payload | 1.2 MB | 400 KB | 67% smaller |
Key Takeaways
- Measure First: Use Lighthouse, WebPageTest, and Chrome DevTools to identify bottlenecks before optimizing
- Start with Low-Hanging Fruit: Image optimization and code splitting provide the biggest wins with minimal effort
- Don't Optimize Prematurely: Profile first, then optimize the actual bottlenecks
- Monitor Continuously: Set up performance budgets and CI checks to prevent regressions
- User Experience Matters: A fast site improves engagement, SEO, and conversions
Tools I Recommend
- Lighthouse: Built into Chrome DevTools
- WebPageTest: Detailed waterfall analysis
- React DevTools Profiler: Identify slow renders
- webpack-bundle-analyzer: Visualize bundle composition
- Vercel Analytics: Real-world performance monitoring
Conclusion
Performance optimization is an ongoing journey, not a destination. By implementing these strategies incrementally, I was able to dramatically improve my portfolio's performance without sacrificing functionality or user experience.
Remember: Every millisecond counts. Users notice fast sites, and Google rewards them in search rankings. The investment in performance optimization pays dividends in user satisfaction and business outcomes.
Have questions about React performance optimization? Feel free to reach out via the contact form or connect with me on LinkedIn.