A full-stack engineer's performance optimization journey, including all pitfalls and debugging process
Background: The User Feedback "Performance Crisis"
Last month, our LitReview-AI (academic research AI tool) received several concerning user feedback messages:
"The homepage loads too slowly, I have to wait several seconds every time to see content"
"As a researcher, time is precious, waiting this long seriously affects the user experience"
"Compared to other tools, your loading speed is indeed a bit slow"
After reading this feedback, I had mixed feelings. As a modern application built on Next.js 15 + React 19, performance issues shouldn't be this severe.
⚠️ Wake-up call: User patience is limited, and performance issues directly impact user experience and retention rates.
So I immediately conducted a comprehensive Lighthouse test.
Problem Diagnosis: Shocking Data
The first Lighthouse test results left me stunned:
| Metric | Actual Value | Target Value | Status |
|---|---|---|---|
| LCP (Largest Contentful Paint) | 4.2s | < 2.5s | ❌ |
| FCP (First Contentful Paint) | 1.2s | < 1.8s | ⚠️ |
| TTI (Time to Interactive) | 3.8s | < 3.8s | ❌ |
| Bundle Size | 2.8MB | < 1.5MB | ❌ |
Critical Issue: LCP time severely exceeded standards, directly affecting users' perception of website loading speed.
Even worse, Google Analytics data showed users' average dwell time was only 45 seconds, with a bounce rate as high as 68%.
❌ Fatal Data: For an academic tool that requires deep user engagement, a 68% bounce rate is nearly unacceptable.
Week One: Failed Attempts at Blind Optimization
Error 1: Abusing Dynamic Imports Causing Page Flickering
Seeing the bundle size exceeded standards, my first reaction was to use dynamic imports extensively:
// ❌ Wrong approach - excessive dynamic imports
const HomePage = dynamic(() => import('@/components/HomePage'), {
loading: () => <div>Loading...</div>
})
const AnalysisInterface = dynamic(() => import('@/components/AnalysisInterface'))
const Testimonials = dynamic(() => import('@/components/Testimonials'))
Problem Analysis: I mistakenly dynamically imported above-the-fold core components, which caused obvious white screens during page loading.
As expected: the page showed obvious white screens and layout shifts, making the user experience even worse.
❌ Painful Lesson: LCP not only didn't improve but actually worsened to 4.5s due to component loading order issues.
💡 Core Principle: Above-the-fold core components should not be dynamically imported, as this delays rendering of critical content.
Error 2: Incorrect Image Optimization Configuration
I noticed issues with the Next.js Image component configuration, so I started adjusting:
// ❌ Wrong image configuration
<Image
src="/demo.webm"
width={800}
height={450}
quality={20} // Too low!
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
Fatal Error: In pursuit of loading speed, I set quality to 20, which directly caused the demo video to be blurry.
This configuration completely lost its presentation effect. Users reported they couldn't see the demo content clearly, almost ruining an important feature.
❌ Profound Lesson: Performance optimization cannot come at the cost of sacrificing core functionality experience. Low-quality content is worse than slow loading.
Week Two: Systematic Performance Analysis
Learning from my mistakes, I decided to adopt a more systematic approach. First, I installed professional analysis tools:
npm install --save-dev @next/bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
Tool Description: bundle-analyzer helps us clearly see which modules take up the most space.
Real Problems Discovered Through Analysis
Through detailed analysis, I discovered four main issues:
| Problem Type | Specific Issue | Impact | Solution Priority |
|---|---|---|---|
| Oversized Bundle | lodash full import | 420KB unused code | 🔥 High |
| Script Blocking | Third-party scripts sync loading | Blocks main thread | 🔥 High |
| Font Loading | Inter font not preloaded | FOUT flickering | ⚠️ Medium |
| Image Optimization | Demo video direct loading | File too large | ⚠️ Medium |
💡 Important Discovery: lodash full import was the biggest problem; solving it would reduce nearly 500KB of bundle size.
Week Three: Precise Optimization Solutions
1. Bundle Optimization: From 2.8MB to 1.2MB
Solving the lodash problem:
// ❌ Before
import _ from 'lodash'
const sorted = _.sortBy(data, 'date')
// ✅ Now
import { sortBy } from 'lodash-es'
const sorted = sortBy(data, 'date')
// Or even better, use native methods
const sorted = data.sort((a, b) => new Date(a.date) - new Date(b.date))
Enable package optimization in next.config.ts:
const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons', 'date-fns'],
},
// Enable tree shaking
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization = {
...config.optimization,
usedExports: true,
sideEffects: true,
};
}
return config;
},
}
2. Image Loading Optimization
Correct image configuration:
// ✅ Correct image optimization configuration
import Image from 'next/image'
import { useState } from 'react'
export function OptimizedDemoVideo() {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
{isLoading && (
<div className="absolute inset-0 bg-gray-100 animate-pulse rounded-lg" />
)}
<Image
src="/demo.webm"
alt="Demo video showing LitReview AI in action"
width={800}
height={450}
className="rounded-lg"
onLoad={() => setIsLoading(false)}
priority // Above-the-fold image
placeholder="blur"
quality={85} // Balance quality and size
/>
</div>
)
}
3. Font Loading Optimization
// layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Avoid FOUT
preload: true,
fallback: ['system-ui', 'sans-serif'],
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.className}>
<head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
)
}
4. Third-Party Script Optimization
// ✅ Lazy load analytics scripts
import Script from 'next/script'
export function AnalyticsScripts() {
return (
<>
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
strategy="afterInteractive"
/>
<Script
id="google-analytics"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`,
}}
/>
<Script
src="https://static.hotjar.com/c/hotjar-XXXXXX.js"
strategy="lazyOnload" // Load after page is idle
/>
</>
)
}
5. Key Performance Monitoring
I developed a performance monitoring component to track key metrics in real-time:
// PerformanceMonitor.tsx
export function PerformanceMonitor() {
const [metrics, setMetrics] = useState({
lcp: null,
fid: null,
cls: null,
ttfb: null,
})
useEffect(() => {
// Monitor LCP
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
setMetrics(prev => ({ ...prev, lcp: lastEntry.startTime }))
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
// ... Other monitoring logic
}, [])
// Only show in development environment
if (process.env.NODE_ENV !== 'development') return null
return (
<div className="fixed bottom-4 right-4 bg-white border rounded-lg shadow-lg p-3 text-xs">
<div className="font-semibold mb-2">⚡ Performance Monitor</div>
<div className="space-y-1">
<div>LCP: {Math.round(metrics.lcp)}ms</div>
<div>FID: {Math.round(metrics.fid)}ms</div>
<div>CLS: {metrics.cls?.toFixed(3)}</div>
<div>TTFB: {Math.round(metrics.ttfb)}ms</div>
</div>
</div>
)
}
6. Cache Strategy Optimization
Configured detailed cache strategy in next.config.ts:
async headers() {
return [
{
source: '/:all*(svg|jpg|jpeg|png|gif|ico|webp)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/api/health',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=30',
},
],
},
]
}
Final Results: Quantified Improvements
After three weeks of systematic optimization, we achieved significant improvements:
| Metric | Before | After | Improvement |
|---|---|---|---|
| LCP | 4.2s | 1.8s | 57.1% ↓ |
| FCP | 1.2s | 0.6s | 50% ↓ |
| TTI | 3.8s | 1.5s | 60.5% ↓ |
| Bundle Size | 2.8MB | 1.2MB | 57.1% ↓ |
| User Dwell Time | 45s | 2m15s | 200% ↑ |
More importantly, user feedback became positive:
"The loading speed is much faster now, great experience!" "Page response is very smooth, work efficiency has improved significantly"
Experience Summary and Recommendations
1. Diagnostic Tools First
Don't optimize blindly, use tools to find the real problems first:
- Lighthouse (comprehensive performance analysis)
- WebPageTest (network environment testing)
- Bundle Analyzer (bundle size analysis)
- Chrome DevTools Performance (runtime analysis)
2. Optimization Order Matters
- First solve Bundle Size: This is the easiest to see results from
- Then optimize Image Loading: Has the biggest impact on LCP
- Next handle Fonts and Scripts: Reduce blocking
- Finally implement Cache Strategy: Improve subsequent visits
3. Traps to Avoid
- ❌ Overusing dynamic imports causing page flickering
- ❌ Sacrificing core functionality experience for performance
- ❌ Ignoring mobile performance (60% of users come from mobile)
- ❌ Deploying optimizations without A/B testing
4. Continuous Monitoring
Performance optimization is not a one-time job:
// Also collect performance data in production
if ('performance' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Send to analytics service
analytics.track('performance_metric', {
name: entry.name,
value: entry.value,
})
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
Specific Recommendations for Next.js Developers
Based on this optimization experience, I've summarized several recommendations for fellow developers:
- Use Next.js 15 New Features
// Enable package optimization
experimental: {
optimizePackageImports: ['lucide-react', 'date-fns'],
}
- Use Dynamic Imports Reasonably
// ✅ Only use dynamic imports for non-above-the-fold components
const AdminPanel = dynamic(() => import('@/components/AdminPanel'))
const Charts = dynamic(() => import('@/components/Charts'))
- Thorough Image Optimization
// Use priority to mark above-the-fold images
<Image
src="/hero.jpg"
priority
sizes="(max-width: 768px) 100vw, 50vw"
/>
- Correct Font Strategy
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
})
Final Thoughts
Performance optimization is a continuous process that requires combining data analysis and user feedback. This optimization improved our application performance by 57%, and more importantly, enhanced user experience.
For fellow developers working on performance optimization, I want to say: don't be intimidated by complex tools, start small, use data to guide your decisions, and continuously improve. Every millisecond of improvement can translate into user satisfaction and retention rates.
Remember: Performance is not a feature, but the foundation of experience.
This article is based on LitReview-AI's real optimization experience, with all data and code from production environment. The project uses Next.js 15.3.4 + React 19 + TypeScript, deployed on Vercel.
Related Links:
- Project: https://litreview-ai.com
- Performance Monitoring Tools: https://web.dev/vitals/
- Next.js Performance Optimization Documentation: https://nextjs.org/docs/advanced-features/measuring-performance