Skip to main content
Performance is critical for user experience and SEO. This guide covers the performance optimization strategies and tools used in the monorepo.

Core Web Vitals

Both applications automatically track Core Web Vitals metrics:
  • LCP (Largest Contentful Paint) - Loading performance
  • INP (Interaction to Next Paint) - Interactivity
  • CLS (Cumulative Layout Shift) - Visual stability
  • FCP (First Contentful Paint) - Initial rendering
  • TTFB (Time to First Byte) - Server response time

Next.js Web Vitals (@workspace/web)

The Next.js app uses the useReportWebVitals hook:
// apps/web/src/core/providers/web-vitals.client.tsx
'use client'

import { logger } from '@workspace/core/utils/logger'
import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Log metrics for monitoring
    logger.log(`[web-vitals]: ${metric.name}`, metric)
    
    // Send to analytics if rating is not "good"
    if (metric.rating !== 'good') {
      sendToAnalytics(metric)
    }
  })

  return null
}
Include the component in your root layout:
import { WebVitals } from '@/core/providers/web-vitals.client'

export function Providers({ children }) {
  return (
    <>
      <WebVitals />
      {children}
    </>
  )
}

React SPA Web Vitals (@workspace/spa)

The SPA uses the web-vitals library with OpenTelemetry integration:
// apps/spa/src/core/utils/web-vitals.ts
import { metrics } from '@opentelemetry/api'
import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals'

const meter = metrics.getMeter('web-vitals')

const lcpMetric = meter.createHistogram('web_vitals_lcp', {
  description: 'Largest Contentful Paint',
  unit: 'ms',
})

const inpMetric = meter.createHistogram('web_vitals_inp', {
  description: 'Interaction to Next Paint',
  unit: 'ms',
})

// Record metrics
export function reportWebVitals() {
  onLCP((metric) => {
    lcpMetric.record(metric.value, {
      delta: metric.delta,
      navigationType: metric.navigationType,
      rating: metric.rating,
    })
  })
  
  onINP((metric) => {
    inpMetric.record(metric.value, {
      delta: metric.delta,
      navigationType: metric.navigationType,
      rating: metric.rating,
    })
  })
  
  // ... CLS, FCP, TTFB
}
Call reportWebVitals() on key pages:
import { reportWebVitals } from '@/core/utils/web-vitals'

function HomePage() {
  useEffect(() => {
    reportWebVitals()
  }, [])
  
  return <div>Home Content</div>
}
Web Vitals metrics are sent to Grafana via OpenTelemetry. View them in your Prometheus data source.

Bundle Analysis

Next.js Bundle Analysis

Analyze your Next.js bundle to identify optimization opportunities:
cd apps/web
NEXT_ANALYZE=true bun run build
This generates an interactive bundle analyzer report showing:
  • Module sizes
  • Tree-shaking effectiveness
  • Code splitting efficiency
  • Duplicate dependencies

React SPA Bundle Analysis

The SPA uses rollup-plugin-visualizer for bundle analysis:
// apps/spa/vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'html/visualizer-stats.html',
    }),
  ],
})
After building, open html/visualizer-stats.html to explore:
  • Chunk sizes
  • Module dependencies
  • Tree map visualization
  • Redundant code
cd apps/spa
bun run build
open html/visualizer-stats.html

Optimization Techniques

Code Splitting

Use dynamic imports with next/dynamic:
import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Skeleton />,
  ssr: false, // Disable SSR if not needed
})

function Page() {
  return <HeavyComponent />
}

Image Optimization

Use the optimized Image component:
import Image from 'next/image'

function Gallery() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={630}
      priority // For above-the-fold images
      placeholder="blur"
      blurDataURL="data:image/..."
    />
  )
}
Next.js automatically:
  • Serves WebP/AVIF formats
  • Resizes images on-demand
  • Lazy loads by default
  • Prevents CLS with explicit dimensions

Font Optimization

Use next/font for automatic font optimization:
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

export default function RootLayout({ children }) {
  return (
    <html className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}
Benefits:
  • Zero layout shift
  • Self-hosted fonts
  • Automatic subsetting
  • Preloading

React Compiler

Both applications use the React Compiler for automatic memoization:
// next.config.ts (web)
const config = {
  reactCompiler: true,
}

// vite.config.ts (spa)
react({
  babel: {
    plugins: ['babel-plugin-react-compiler'],
  },
})
The compiler automatically:
  • Memoizes components
  • Optimizes re-renders
  • Reduces manual useMemo/useCallback usage
  • Improves overall performance

Package Optimization

Optimize dependencies to reduce bundle size:
// next.config.ts
const config = {
  experimental: {
    optimizePackageImports: ['@workspace/core'],
  },
}
Ensure your imports support tree-shaking:
// ✅ Good - tree-shakeable
import { Button } from '@workspace/core'

// ❌ Bad - imports entire package
import * as Core from '@workspace/core'
Load heavy libraries only when needed:
async function processCSV(file: File) {
  const Papa = await import('papaparse')
  return Papa.parse(file)
}
Check for duplicate or outdated packages:
bun run bundle-analyzer

Caching Strategies

Next.js

Leverage Next.js caching mechanisms:
// Static generation with revalidation
export const revalidate = 3600 // Revalidate every hour

// Route segment config
export const dynamic = 'force-static'
export const fetchCache = 'force-cache'

// Fetch with caching
fetch('https://api.example.com/data', {
  next: { revalidate: 3600 },
})

Service Workers (SPA)

The SPA uses Vite PWA for offline caching:
// vite.config.ts
VitePWA({
  strategies: 'generateSW',
  registerType: 'prompt',
  workbox: {
    globPatterns: ['**/*.{html,css,js,json,txt,ico,svg,jpg,png,webp,woff,woff2}'],
    cleanupOutdatedCaches: true,
    clientsClaim: true,
  },
})

Performance Monitoring Tools

Capo.js

Optimize HTML <head> element ordering for better performance

Unlighthouse

Scan and measure performance across all pages

Web Vitals

Learn about Core Web Vitals and optimization

Web.dev Performance

Comprehensive performance learning guide

Performance Checklist

1

Measure baseline

  • Run Lighthouse audits
  • Check Web Vitals scores
  • Analyze bundle sizes
  • Review network waterfalls
2

Optimize critical path

  • Minimize blocking resources
  • Inline critical CSS
  • Preload key resources
  • Use font-display: swap
3

Reduce bundle size

  • Analyze and remove unused code
  • Code-split large dependencies
  • Use dynamic imports
  • Enable compression (gzip/brotli)
4

Optimize images

  • Use modern formats (WebP/AVIF)
  • Implement lazy loading
  • Add explicit dimensions
  • Use responsive images
5

Monitor continuously

  • Set up Web Vitals tracking
  • Monitor with Grafana
  • Run automated Lighthouse CI
  • Track bundle size in CI/CD

Best Practices

Load critical content first:
  • Use priority prop on hero images
  • Inline critical CSS
  • Defer non-critical JavaScript
  • Preload fonts and key assets
Build a solid foundation:
  • Start with HTML
  • Layer on CSS
  • Enhance with JavaScript
  • Ensure core functionality without JS
Configure aggressive caching for static assets:
Cache-Control: public, max-age=31536000, immutable
And short-lived caching for dynamic content:
Cache-Control: public, max-age=0, must-revalidate
Track actual user experience:
  • Set up RUM (Real User Monitoring)
  • Analyze field data, not just lab data
  • Segment by device, network, geography
  • Act on performance budgets

Resources

Next.js Performance

Official Next.js optimization guide

Vite Performance

Vite performance best practices

web.dev

Google’s web development best practices

Grafana Observability

Monitor performance with Grafana

Build docs developers (and LLMs) love