name: performance-optimization
description: Expert guide for optimizing Next.js performance - images, fonts, code splitting, caching, and Core Web Vitals. Use when improving load times or debugging performance issues.
slug: performance-optimization
category: operations
complexity: complex
version: "1.0.0"
author: "id8Labs"
triggers:
- "performance-optimization"
- "performance optimization"
tags:
- development
- tool-factory-retrofitted---
Performance Optimization Skill
Core Workflows
Workflow 1: Primary Action
- Analyze the input and context
- Validate prerequisites are met
- Execute the core operation
- Verify the output meets expectations
- Report results
Overview
This skill helps you optimize your Next.js application for maximum performance. From image optimization to code splitting, this covers all the techniques you need to achieve excellent Core Web Vitals scores.
Core Web Vitals
1. Largest Contentful Paint (LCP)
Target: < 2.5s
Optimize:
- Use
next/image for images
- Implement proper caching
- Use CDN for static assets
- Optimize server response time
- Reduce render-blocking resources
2. First Input Delay (FID) / Interaction to Next Paint (INP)
Target: < 100ms / < 200ms
Optimize:
- Minimize JavaScript execution
- Code split large bundles
- Use Web Workers for heavy tasks
- Defer non-critical JavaScript
- Optimize event handlers
3. Cumulative Layout Shift (CLS)
Target: < 0.1
Optimize:
- Set image dimensions
- Reserve space for ads
- Avoid inserting content above existing content
- Use
transform instead of layout properties
Image Optimization
Next.js Image Component
import Image from 'next/image'
export function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // For above-fold images
quality={85} // Default: 75
placeholder="blur"
blurDataURL="data:image/..." // Or import for static
/>
)
}
module.exports = {
images: {
domains: ['example.com'],
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
],
},
}
Responsive Images
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
priority
/>
Image Formats
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
Font Optimization
Using next/font
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
}
Local Fonts
import localFont from 'next/font/local'
const customFont = localFont({
src: './fonts/custom-font.woff2',
display: 'swap',
variable: '--font-custom',
})
Code Splitting
Dynamic Imports
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('@/components/heavy-component'), {
loading: () => <p>Loading...</p>,
ssr: false,
})
export function Page() {
return (
<div>
<HeavyComponent />
</div>
)
}
Conditional Loading
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/chart'), {
ssr: false,
})
export function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && <Chart />}
</div>
)
}
Named Exports
const ComponentA = dynamic(() =>
import('@/components/bundle').then((mod) => mod.ComponentA)
)
React Optimization
React.memo
import { memo } from 'react'
const ExpensiveComponent = memo(function ExpensiveComponent({
data,
}: {
data: Data
}) {
return <div>{/* Expensive rendering */}</div>
})
const MemoizedComponent = memo(
Component,
(prevProps, nextProps) => {
return prevProps.id === nextProps.id
}
)
useMemo
'use client'
import { useMemo } from 'react'
export function DataTable({ items }: { items: Item[] }) {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items])
return (
<table>
{sortedItems.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
</tr>
))}
</table>
)
}
useCallback
'use client'
import { useCallback, useState } from 'react'
export function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return <Child onClick={handleClick} />
}
Caching Strategies
API Route Caching
export async function GET() {
const data = await fetchData()
return Response.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
},
})
}
Data Fetching Caching
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 },
})
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache',
})
Tag-based Revalidation
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
import { revalidateTag } from 'next/cache'
export async function POST() {
await createPost()
revalidateTag('posts')
}
Bundle Optimization
Analyze Bundle
{
"scripts": {
"analyze": "ANALYZE=true next build"
}
}
npm install @next/bundle-analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
})
Tree Shaking
import _ from 'lodash'
import debounce from 'lodash/debounce'
Remove Unused Dependencies
npx depcheck
npm uninstall unused-package
Streaming and Suspense
Streaming Components
import { Suspense } from 'react'
async function SlowComponent() {
const data = await slowFetch()
return <div>{data}</div>
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
)
}
Multiple Suspense Boundaries
export default function Page() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</div>
)
}
Database Query Optimization
Use Prisma Efficiently
const users = await prisma.user.findMany()
for (const user of users) {
const posts = await prisma.post.findMany({ where: { userId: user.id } })
}
const users = await prisma.user.findMany({
include: {
posts: true,
},
})
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: {
select: {
id: true,
title: true,
},
},
},
})
Database Indexes
model Post {
id String @id @default(cuid())
title String
userId String
createdAt DateTime @default(now())
// Add indexes for frequently queried fields
@@index([userId])
@@index([createdAt])
@@index([userId, createdAt])
}
Prerendering Strategies
Static Site Generation (SSG)
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>{/* Render data */}</div>
}
Incremental Static Regeneration (ISR)
export const revalidate = 60
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>{/* Render data */}</div>
}
Dynamic Rendering
export const dynamic = 'force-dynamic'
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
return <div>{/* Render data */}</div>
}
Lazy Loading
Images
<Image
src="/image.jpg"
alt="Image"
width={500}
height={300}
loading="lazy"
/>
Components on Scroll
'use client'
import { useEffect, useState, useRef } from 'react'
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('@/components/heavy'))
export function LazySection() {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (ref.current) {
observer.observe(ref.current)
}
return () => observer.disconnect()
}, [])
return (
<div ref={ref}>
{isVisible ? <HeavyComponent /> : <div>Loading...</div>}
</div>
)
}
Performance Monitoring
Measuring Performance
'use client'
import { useEffect } from 'react'
export function PerformanceMonitor() {
useEffect(() => {
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime)
}).observe({ type: 'largest-contentful-paint', buffered: true })
new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime)
})
}).observe({ type: 'first-input', buffered: true })
let clsScore = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value
}
}
console.log('CLS:', clsScore)
}).observe({ type: 'layout-shift', buffered: true })
}, [])
return null
}
Best Practices Checklist
When to Use This Skill
Invoke this skill when:
- Optimizing page load times
- Improving Core Web Vitals scores
- Reducing bundle size
- Debugging performance issues
- Setting up caching strategies
- Implementing lazy loading
- Optimizing images or fonts
- Improving database query performance