Skip to main contentSkip to navigationSkip to search

How I Improved Next.js App Performance by 57%: From 4.2s to 1.8s

9 min read

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:

MetricActual ValueTarget ValueStatus
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 Size2.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 TypeSpecific IssueImpactSolution Priority
Oversized Bundlelodash full import420KB unused code🔥 High
Script BlockingThird-party scripts sync loadingBlocks main thread🔥 High
Font LoadingInter font not preloadedFOUT flickering⚠️ Medium
Image OptimizationDemo video direct loadingFile 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:

MetricBeforeAfterImprovement
LCP4.2s1.8s57.1% ↓
FCP1.2s0.6s50% ↓
TTI3.8s1.5s60.5% ↓
Bundle Size2.8MB1.2MB57.1% ↓
User Dwell Time45s2m15s200% ↑

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

  1. First solve Bundle Size: This is the easiest to see results from
  2. Then optimize Image Loading: Has the biggest impact on LCP
  3. Next handle Fonts and Scripts: Reduce blocking
  4. 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:

  1. Use Next.js 15 New Features
// Enable package optimization
experimental: {
  optimizePackageImports: ['lucide-react', 'date-fns'],
}
  1. 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'))
  1. Thorough Image Optimization
// Use priority to mark above-the-fold images
<Image
  src="/hero.jpg"
  priority
  sizes="(max-width: 768px) 100vw, 50vw"
/>
  1. 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:

相关文章

8 min

Advanced React Performance Optimization: How We Reduced Re-renders by 73%

ReactPerformance
阅读全文
9 min

Database Optimization: PostgreSQL Performance Tuning That Reduced Query Time by 80%

PostgreSQLDatabase
阅读全文