Performance isn't a nice-to-have. Every 100ms of page load time costs roughly 1% in conversions. For a business doing $100K/month in revenue, a 1-second delay is costing $12,000/year in lost sales.
In this guide, Muhammad Hassan breaks down the battle-tested performance techniques used to achieve 100/100 Core Web Vitals across production web apps.
1. Image Optimization — The Biggest Win
Images are almost always the biggest LCP bottleneck. Next.js's built-in <Image> component handles this well, but most developers don't configure it properly:
- Always set explicit
widthandheightto prevent layout shift (CLS) - Use
priorityprop on above-the-fold images (hero, LCP candidate) - Use
sizesprop to serve different resolutions per viewport - Prefer
WebPorAVIF(Next.js auto-converts) - Lazy-load below-the-fold images (default behaviour, but don't override it)
2. Font Strategy — Don't Block Render
Google Fonts is convenient but can add 200–400ms to LCP if not handled carefully. Muhammad Hassan's font delivery approach:
- Use
next/font(built-in) — it downloads fonts at build time and serves them from your domain - Always use
display: swapto prevent invisible text during load - Only load the font weights and styles you actually use
- Subset fonts to your language characters
3. Route-Level Code Splitting
Next.js automatically splits code by route, but you need to actively avoid leaking heavy dependencies into the shared bundle:
- Use dynamic imports (
next/dynamic) for heavy components that aren't needed immediately - Move charting libraries, rich text editors, and animations to dynamic imports
- Use
ssr: falsefor client-only components to avoid hydration overhead
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false,
});
4. Caching Strategy
Understanding Next.js caching layers is critical:
- Static pages — use
generateStaticParamsfor known routes - Fetch caching — all server-side fetches are cached by default; use
revalidateto control TTL - Edge caching — deploy to Vercel Edge Network or Cloudflare Workers for sub-50ms global latency
- Data Cache — use React's
cache()for deduplicated data fetching within a render
5. Bundle Analysis
Before you can fix bundle bloat, you need to see it:
npm install --save-dev @next/bundle-analyzer
# In next.config.js:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
Run ANALYZE=true npm run build and identify the culprits. Common offenders: moment.js (replace with date-fns), lodash (use lodash-es with tree shaking), and heavy icon libraries (import only what you need).
6. Streaming and Suspense
For pages with slow data dependencies, use React Suspense to stream partial HTML while data loads. This gets something meaningful on screen immediately:
export default function Page() {
return (
<>
<StaticHero />
<Suspense fallback={<SkeletonGrid />}>
<AsyncProjectGrid /> {/* Fetches data server-side */}
</Suspense>
</>
)
}
7. Prefetching
Next.js's <Link> prefetches by default when the link enters the viewport. Maximise this by:
- Using
<Link>instead of<a>for all internal navigation - Using
router.prefetch('/route')proactively on hover for CTAs - Keeping critical above-the-fold links in the initial HTML (not hidden behind JS)
Results You Can Expect
Applying all of the above consistently, I (Muhammad Hassan) regularly achieve:
- Lighthouse Performance: 94–99
- LCP: < 1.8s on mobile
- CLS: < 0.05
- INP: < 150ms
- FCP: < 0.8s
These aren't just metrics — they're directly correlated with conversion rates, SEO rankings, and user retention. Build fast, ship fast, convert more. Need an expert performance review? Work with Muhammad Hassan.
