Skip to main content

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