Chapter 9: Advanced Topics and Optimization
What are Advanced Next.js Topics?
Advanced Next.js topics cover performance optimization, middleware, custom configurations, and production-ready features that help you build scalable, maintainable applications. These topics are essential for taking your Next.js skills to the expert level.
Advanced Topics Covered:
- Performance Optimization: Image optimization, code splitting, caching
- Middleware: Request/response processing, authentication, redirects
- Custom Configurations: Webpack, Babel, and Next.js config customization
- Production Features: Monitoring, analytics, error tracking
- Advanced Patterns: Custom hooks, context optimization, state management
Why Master Advanced Topics?
Benefits:
- Performance: Faster loading times and better user experience
- Scalability: Handle high traffic and complex applications
- Maintainability: Clean, organized, and testable code
- Production Ready: Robust error handling and monitoring
- Developer Experience: Better tooling and debugging capabilities
How to Implement Advanced Features
1. Performance Optimization
Image Optimization:
// components/OptimizedImage.js
import Image from 'next/image'
export default function OptimizedImage({ src, alt, width, height, priority = false }) {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
quality={85}
/>
)
}
// Usage
export default function Gallery() {
return (
<div className="gallery">
<OptimizedImage
src="/hero-image.jpg"
alt="Hero image"
width={800}
height={600}
priority={true}
/>
<OptimizedImage
src="/gallery-1.jpg"
alt="Gallery image 1"
width={400}
height={300}
/>
</div>
)
}
Code Splitting and Dynamic Imports:
// components/LazyComponent.js
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
// Lazy load heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <div>Loading chart...</div>,
ssr: false, // Disable SSR for client-only components
})
const MapComponent = dynamic(() => import('./MapComponent'), {
loading: () => <div>Loading map...</div>,
})
// Conditional loading
const AdminPanel = dynamic(() => import('./AdminPanel'), {
loading: () => <div>Loading admin panel...</div>,
})
export default function Dashboard({ user }) {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading charts...</div>}>
<HeavyChart data={chartData} />
</Suspense>
<MapComponent />
{user.isAdmin && (
<Suspense fallback={<div>Loading admin panel...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
)
}
Bundle Analysis:
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Your Next.js config
experimental: {
optimizeCss: true,
},
webpack: (config, { dev, isServer }) => {
// Custom webpack configuration
if (!dev && !isServer) {
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
}
}
return config
},
})
2. Middleware Implementation
Basic Middleware:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// Get the pathname of the request
const { pathname } = request.nextUrl
// Check if the request is for a protected route
if (pathname.startsWith('/dashboard')) {
// Check for authentication token
const token = request.cookies.get('auth-token')
if (!token) {
// Redirect to login page
return NextResponse.redirect(new URL('/login', request.url))
}
}
// Check for maintenance mode
if (process.env.MAINTENANCE_MODE === 'true' && pathname !== '/maintenance') {
return NextResponse.redirect(new URL('/maintenance', request.url))
}
// Add security headers
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
response.headers.set('X-XSS-Protection', '1; mode=block')
return response
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Advanced Middleware with Authentication:
// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from './lib/auth'
export async function middleware(request) {
const { pathname } = request.nextUrl
const response = NextResponse.next()
// Handle API routes
if (pathname.startsWith('/api/')) {
// Add CORS headers for API routes
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { status: 200, headers: response.headers })
}
// Check authentication for protected API routes
if (pathname.startsWith('/api/protected/')) {
try {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
return NextResponse.json({ error: 'No token provided' }, { status: 401 })
}
const user = await verifyToken(token)
request.user = user
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
}
}
// Handle page routes
if (!pathname.startsWith('/api/')) {
// Redirect based on user authentication status
const token = request.cookies.get('auth-token')
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url))
}
if (pathname.startsWith('/login') && token) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// Add performance headers
response.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300')
}
return response
}
Rate Limiting Middleware:
// middleware.js
import { NextResponse } from 'next/server'
const rateLimitMap = new Map()
function rateLimit(identifier, limit = 100, windowMs = 60 * 1000) {
const now = Date.now()
const windowStart = now - windowMs
if (!rateLimitMap.has(identifier)) {
rateLimitMap.set(identifier, [])
}
const requests = rateLimitMap.get(identifier)
const validRequests = requests.filter(time => time > windowStart)
if (validRequests.length >= limit) {
return false
}
validRequests.push(now)
rateLimitMap.set(identifier, validRequests)
return true
}
export function middleware(request) {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'
const pathname = request.nextUrl.pathname
// Apply rate limiting to API routes
if (pathname.startsWith('/api/')) {
const isAllowed = rateLimit(ip, 100, 60 * 1000) // 100 requests per minute
if (!isAllowed) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
}
return NextResponse.next()
}
3. Custom Configurations
Advanced Next.js Configuration:
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
})
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(withPWA({
// Performance optimizations
experimental: {
optimizeCss: true,
optimizePackageImports: ['@mui/material', 'lodash'],
},
// Image optimization
images: {
domains: ['example.com', 'cdn.example.com'],
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Webpack customization
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Custom webpack plugins
if (!dev && !isServer) {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.BUILD_ID': JSON.stringify(buildId),
})
)
}
// Optimize bundle splitting
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
chunks: 'all',
},
common: {
name: 'common',
minChunks: 2,
priority: -5,
reuseExistingChunk: true,
},
},
}
return config
},
// Headers configuration
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
],
},
{
source: '/api/(.*)',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization',
},
],
},
]
},
// Redirects
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
{
source: '/blog/:slug',
destination: '/posts/:slug',
permanent: false,
},
]
},
// Rewrites
async rewrites() {
return [
{
source: '/api/external/:path*',
destination: 'https://external-api.com/:path*',
},
]
},
// Environment variables
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
}))
4. Advanced State Management
Context Optimization:
// contexts/AppContext.js
import { createContext, useContext, useReducer, useMemo } from 'react'
const AppContext = createContext()
// Split contexts for better performance
const UserContext = createContext()
const ThemeContext = createContext()
const CartContext = createContext()
// Optimized reducer
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload }
case 'SET_THEME':
return { ...state, theme: action.payload }
case 'ADD_TO_CART':
return {
...state,
cart: [...state.cart, action.payload]
}
case 'REMOVE_FROM_CART':
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload)
}
default:
return state
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: 'light',
cart: [],
})
// Memoize context values to prevent unnecessary re-renders
const userValue = useMemo(() => ({
user: state.user,
setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
}), [state.user])
const themeValue = useMemo(() => ({
theme: state.theme,
setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
}), [state.theme])
const cartValue = useMemo(() => ({
cart: state.cart,
addToCart: (item) => dispatch({ type: 'ADD_TO_CART', payload: item }),
removeFromCart: (id) => dispatch({ type: 'REMOVE_FROM_CART', payload: id }),
}), [state.cart])
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<CartContext.Provider value={cartValue}>
{children}
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
)
}
// Custom hooks for each context
export const useUser = () => {
const context = useContext(UserContext)
if (!context) {
throw new Error('useUser must be used within UserProvider')
}
return context
}
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
export const useCart = () => {
const context = useContext(CartContext)
if (!context) {
throw new Error('useCart must be used within CartProvider')
}
return context
}
Custom Hooks for Performance:
// hooks/useDebounce.js
import { useState, useEffect } from 'react'
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react'
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
return [storedValue, setValue]
}
// hooks/useIntersectionObserver.js
import { useEffect, useRef, useState } from 'react'
export function useIntersectionObserver(options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false)
const [hasIntersected, setHasIntersected] = useState(false)
const ref = useRef()
useEffect(() => {
const element = ref.current
if (!element) return
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting)
if (entry.isIntersecting && !hasIntersected) {
setHasIntersected(true)
}
}, options)
observer.observe(element)
return () => {
observer.unobserve(element)
}
}, [options, hasIntersected])
return [ref, isIntersecting, hasIntersected]
}
5. Production Monitoring and Analytics
Error Tracking:
// lib/errorTracking.js
class ErrorTracker {
constructor() {
this.init()
}
init() {
// Track unhandled errors
window.addEventListener('error', (event) => {
this.trackError({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
type: 'javascript_error',
})
})
// Track unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.trackError({
message: event.reason?.message || 'Unhandled promise rejection',
stack: event.reason?.stack,
type: 'promise_rejection',
})
})
}
trackError(error) {
// Send to your error tracking service
if (process.env.NODE_ENV === 'production') {
fetch('/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...error,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
}),
}).catch(console.error)
}
}
trackCustomError(message, context = {}) {
this.trackError({
message,
type: 'custom_error',
context,
})
}
}
export const errorTracker = new ErrorTracker()
// pages/_app.js
import { errorTracker } from '../lib/errorTracking'
export default function MyApp({ Component, pageProps }) {
useEffect(() => {
// Initialize error tracking
errorTracker.init()
}, [])
return <Component {...pageProps} />
}
Performance Monitoring:
// lib/performance.js
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.init()
}
init() {
// Track Core Web Vitals
if (typeof window !== 'undefined') {
this.trackWebVitals()
this.trackPageLoad()
}
}
trackWebVitals() {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(this.sendMetric)
getFID(this.sendMetric)
getFCP(this.sendMetric)
getLCP(this.sendMetric)
getTTFB(this.sendMetric)
})
}
trackPageLoad() {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0]
this.sendMetric({
name: 'page_load_time',
value: navigation.loadEventEnd - navigation.fetchStart,
delta: navigation.loadEventEnd - navigation.fetchStart,
})
})
}
sendMetric = (metric) => {
if (process.env.NODE_ENV === 'production') {
fetch('/api/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...metric,
timestamp: Date.now(),
url: window.location.href,
}),
}).catch(console.error)
}
}
}
export const performanceMonitor = new PerformanceMonitor()
Best Practices
1. Performance Optimization
// ✅ Good: Optimize images and lazy loading
import Image from 'next/image'
export default function OptimizedGallery({ images }) {
return (
<div className="gallery">
{images.map((image, index) => (
<Image
key={image.id}
src={image.src}
alt={image.alt}
width={400}
height={300}
priority={index < 3} // Prioritize first 3 images
loading={index < 3 ? 'eager' : 'lazy'}
/>
))}
</div>
)
}
2. Error Boundaries
// ✅ Good: Comprehensive error boundary
import { Component } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
// Send to error tracking service
if (process.env.NODE_ENV === 'production') {
errorTracker.trackError({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
type: 'react_error_boundary',
})
}
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong.</h2>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
3. Security Best Practices
// ✅ Good: Secure API routes
export default async function handler(req, res) {
// Validate request method
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
// Rate limiting
const rateLimitResult = await rateLimit(req.ip)
if (!rateLimitResult.success) {
return res.status(429).json({ error: 'Too many requests' })
}
// Input validation
const { error, value } = schema.validate(req.body)
if (error) {
return res.status(400).json({ error: error.message })
}
// Sanitize input
const sanitizedData = sanitize(value)
// Process request
try {
const result = await processRequest(sanitizedData)
res.status(200).json(result)
} catch (error) {
console.error('API Error:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
Common Mistakes to Avoid
1. Over-optimization
// ❌ Wrong: Over-optimizing with too many contexts
const UserContext = createContext()
const UserNameContext = createContext()
const UserEmailContext = createContext()
// ✅ Correct: Reasonable context splitting
const UserContext = createContext()
const ThemeContext = createContext()
2. Ignoring Error Handling
// ❌ Wrong: No error handling
export default function Component() {
const [data, setData] = useState(null)
useEffect(() => {
fetchData().then(setData) // Could fail
}, [])
return <div>{data.title}</div>
}
// ✅ Correct: Proper error handling
export default function Component() {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData()
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.title}</div>
}
3. Not Monitoring Performance
// ❌ Wrong: No performance monitoring
export default function HeavyComponent() {
// Heavy operations without monitoring
const processedData = heavyProcessing(data)
return <div>{processedData}</div>
}
// ✅ Correct: Monitor performance
export default function HeavyComponent() {
const [processedData, setProcessedData] = useState(null)
useEffect(() => {
const start = performance.now()
const result = heavyProcessing(data)
const end = performance.now()
performanceMonitor.trackCustomMetric('heavy_processing', end - start)
setProcessedData(result)
}, [data])
return <div>{processedData}</div>
}
Summary
Advanced Next.js topics provide the tools needed to build production-ready applications:
- Performance optimization through image optimization and code splitting
- Middleware for authentication, rate limiting, and security
- Custom configurations for webpack, babel, and Next.js
- State management with optimized context and custom hooks
- Monitoring and analytics for production applications
Key Takeaways:
- Optimize images and implement lazy loading
- Use middleware for cross-cutting concerns
- Implement proper error handling and monitoring
- Optimize bundle size and loading performance
- Follow security best practices
This tutorial is part of the comprehensive Next.js learning path at syscook.dev. Continue to the next chapter to learn about deployment and production.
Author: syscook.dev
Last Updated: December 2024