Performance Optimization Implementation

1. Core Web Vitals LCP CLS FID

Metric Measures Good Target Optimization
LCP (Largest Contentful Paint) Loading performance < 2.5s Optimize images, server response, render-blocking resources
CLS (Cumulative Layout Shift) Visual stability < 0.1 Set image/video dimensions, avoid inserting content, use CSS transforms
FID (First Input Delay) Interactivity < 100ms Reduce JS execution time, code splitting, web workers
INP (Interaction to Next Paint) Responsiveness (replaces FID) < 200ms Optimize event handlers, reduce main thread work
TTFB (Time to First Byte) Server response < 800ms CDN, server optimization, caching
FCP (First Contentful Paint) First render < 1.8s Inline critical CSS, preload fonts, optimize server

Example: Core Web Vitals measurement and optimization

// Measure Core Web Vitals with web-vitals library
npm install web-vitals

import { onCLS, onFID, onLCP, onINP, onFCP, onTTFB } from 'web-vitals';

// Send to analytics
function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  
  // Use sendBeacon if available (doesn't block page unload)
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', body);
  } else {
    fetch('/analytics', { body, method: 'POST', keepalive: true });
  }
}

// Measure all Core Web Vitals
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

// React hook for Core Web Vitals
import { useEffect } from 'react';

function useWebVitals(onMetric) {
  useEffect(() => {
    import('web-vitals').then(({ onCLS, onFID, onLCP }) => {
      onCLS(onMetric);
      onFID(onMetric);
      onLCP(onMetric);
    });
  }, [onMetric]);
}

// Usage
function App() {
  useWebVitals((metric) => {
    console.log(metric.name, metric.value);
  });

  return <div>App</div>;
}

// Optimize LCP - Preload critical resources
<head>
  {/* Preload hero image */}
  <link rel="preload" as="image" href="/hero.jpg" />
  
  {/* Preload critical fonts */}
  <link
    rel="preload"
    as="font"
    href="/fonts/inter.woff2"
    type="font/woff2"
    crossOrigin="anonymous"
  />
  
  {/* Preconnect to external domains */}
  <link rel="preconnect" href="https://api.example.com" />
  <link rel="dns-prefetch" href="https://cdn.example.com" />
</head>

// Optimize CLS - Reserve space for images
<img
  src="image.jpg"
  alt="Description"
  width="800"
  height="600"
  style={{ aspectRatio: '800/600' }}
/>

// Use CSS aspect-ratio
.image-container {
  aspect-ratio: 16 / 9;
}

// Avoid layout shifts from dynamic content
.ad-container {
  min-height: 250px; /* Reserve space */
}

// Use CSS transform instead of top/left
// Bad (causes layout shift)
.element {
  top: 100px;
  transition: top 0.3s;
}

// Good (no layout shift)
.element {
  transform: translateY(100px);
  transition: transform 0.3s;
}

// Optimize FID/INP - Code splitting
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

// Use Web Workers for heavy computation
// worker.js
self.addEventListener('message', (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
});

// main.js
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.addEventListener('message', (e) => {
  console.log('Result:', e.data);
});

// Optimize TTFB - Next.js with CDN
// next.config.js
module.exports = {
  images: {
    domains: ['cdn.example.com'],
  },
  headers: async () => [
    {
      source: '/:path*',
      headers: [
        {
          key: 'Cache-Control',
          value: 'public, max-age=31536000, immutable',
        },
      ],
    },
  ],
};

// Lighthouse CI for continuous monitoring
// .lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000'],
      numberOfRuns: 3,
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'first-input-delay': ['error', { maxNumericValue: 100 }],
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};

// Performance Observer API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime, entry.duration);
  }
});

// Observe different entry types
observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] });

// Custom performance marks
performance.mark('start-api-call');
await fetchData();
performance.mark('end-api-call');

performance.measure('api-call-duration', 'start-api-call', 'end-api-call');

// Next.js built-in Web Vitals reporting
// pages/_app.js
export function reportWebVitals(metric) {
  console.log(metric);
  
  // Send to analytics service
  if (metric.label === 'web-vital') {
    gtag('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  }
}

2. Code Splitting React.lazy Suspense

Technique Implementation Description Use Case
React.lazy lazy(() => import('./Component')) Dynamic import for components Route-based splitting
Suspense <Suspense fallback={...}> Loading boundary for lazy components Show loading state
Route-based Split by routes Load route code on navigation Multi-page apps
Component-based Split heavy components Load on demand (modal, chart) Conditional features
Webpack magic comments /* webpackChunkName: "name" */ Name chunks, prefetch/preload Better caching
Dynamic import import('module').then() Load modules conditionally Polyfills, libraries

Example: Code splitting strategies

// Basic React.lazy with Suspense
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Route-based code splitting with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="/dashboard/*" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Component-based splitting (modal, heavy chart)
function ProductPage() {
  const [showModal, setShowModal] = useState(false);
  
  // Only load modal when needed
  const Modal = lazy(() => import('./Modal'));

  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open</button>
      
      {showModal && (
        <Suspense fallback={<ModalSkeleton />}>
          <Modal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

// Webpack magic comments for chunk naming
const Dashboard = lazy(() =>
  import(/* webpackChunkName: "dashboard" */ './Dashboard')
);

// Prefetch on hover (load before needed)
const Dashboard = lazy(() =>
  import(/* webpackPrefetch: true */ './Dashboard')
);

// Preload (load in parallel with parent)
const Dashboard = lazy(() =>
  import(/* webpackPreload: true */ './Dashboard')
);

// Conditional loading based on feature flags
async function loadFeature() {
  if (featureFlags.newDashboard) {
    const { NewDashboard } = await import('./NewDashboard');
    return NewDashboard;
  } else {
    const { OldDashboard } = await import('./OldDashboard');
    return OldDashboard;
  }
}

// Library splitting (load polyfills conditionally)
async function loadPolyfills() {
  if (!window.IntersectionObserver) {
    await import('intersection-observer');
  }
  
  if (!window.fetch) {
    await import('whatwg-fetch');
  }
}

// Multiple suspense boundaries
function App() {
  return (
    <div>
      <Header />
      
      {/* Critical content */}
      <Suspense fallback={<HeroSkeleton />}>
        <Hero />
      </Suspense>
      
      {/* Non-critical content */}
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  );
}

// Error boundary with lazy loading
import { ErrorBoundary } from 'react-error-boundary';

function LazyRoute({ path, component }) {
  const Component = lazy(component);
  
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Suspense fallback={<Loading />}>
        <Component />
      </Suspense>
    </ErrorBoundary>
  );
}

// Next.js dynamic imports
import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('./Component'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // Disable server-side rendering
});

// With named export
const DynamicChart = dynamic(() =>
  import('./Charts').then(mod => mod.LineChart)
);

// Vite code splitting
// Vite automatically splits by dynamic imports
const AdminPanel = () => import('./AdminPanel.vue');

// Manual chunk configuration (vite.config.js)
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@mui/material', '@emotion/react'],
        },
      },
    },
  },
};

3. React.memo useMemo useCallback

Hook/HOC Purpose When to Use Caveat
React.memo Memoize component Expensive renders, pure components Shallow prop comparison
useMemo Memoize computed value Expensive calculations Don't overuse, memory cost
useCallback Memoize function Pass to memoized children, deps Useful with React.memo
useMemo vs useCallback Value vs function useMemo(() => fn) = useCallback(fn) useCallback is shorthand
Custom compare React.memo(Comp, areEqual) Deep comparison, specific props Complex prop structures

Example: Memoization techniques in React

// React.memo - prevent unnecessary re-renders
import { memo } from 'react';

const ExpensiveComponent = memo(({ data, onClick }) => {
  console.log('Rendering ExpensiveComponent');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id} onClick={() => onClick(item)}>
          {item.name}
        </div>
      ))}
    </div>
  );
});

// Parent component
function Parent() {
  const [count, setCount] = useState(0);
  const [data] = useState([/* large array */]);

  // Without memo, ExpensiveComponent re-renders when count changes
  // With memo, it only re-renders when data or onClick changes
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExpensiveComponent data={data} onClick={handleClick} />
    </div>
  );
}

// useCallback - memoize function reference
function Parent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // Without useCallback, new function on every render
  // const handleClick = (item) => {
  //   console.log(item);
  // };

  // With useCallback, same function reference
  const handleClick = useCallback((item) => {
    console.log(item);
    // Use items from closure
  }, []); // Dependencies

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExpensiveComponent items={items} onClick={handleClick} />
    </div>
  );
}

// useMemo - memoize expensive computation
function DataProcessor({ rawData }) {
  // Without useMemo, processes on every render
  // const processedData = expensiveProcessing(rawData);

  // With useMemo, only recomputes when rawData changes
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return expensiveProcessing(rawData);
  }, [rawData]);

  return <div>{processedData.length} items</div>;
}

// Expensive computation example
const filteredAndSortedList = useMemo(() => {
  return items
    .filter(item => item.active)
    .sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// Custom comparison function for React.memo
const ComplexComponent = memo(
  ({ user, settings }) => {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.settings.theme === nextProps.settings.theme
    );
  }
);

// Real-world example: Search with debounce
function SearchComponent({ onSearch }) {
  const [query, setQuery] = useState('');

  // Memoize debounced function
  const debouncedSearch = useMemo(
    () => debounce((value) => onSearch(value), 300),
    [onSearch]
  );

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  };

  return <input value={query} onChange={handleChange} />;
}

// Context with memoization
const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  // Memoize context value to prevent unnecessary re-renders
  const value = useMemo(
    () => ({
      user,
      setUser,
      isAuthenticated: !!user,
    }),
    [user]
  );

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// When NOT to use memoization
// ❌ Don't memoize everything
function SimpleComponent({ name }) {
  // This is fine without useMemo
  const greeting = `Hello, ${name}!`;
  
  return <div>{greeting}</div>;
}

// ❌ Primitive props don't benefit from React.memo
const SimpleChild = memo(({ text, count }) => {
  // React already optimizes primitive comparisons
  return <div>{text} {count}</div>;
});

// ✅ DO use for expensive renders or large prop objects
const DataGrid = memo(({ data, columns, onSort }) => {
  // Expensive table rendering with thousands of rows
  return <table>...</table>;
});

// Combined example: Optimized list component
const ListItem = memo(({ item, onDelete }) => {
  return (
    <li>
      {item.name}
      <button onClick={() => onDelete(item.id)}>Delete</button>
    </li>
  );
});

function List({ items }) {
  const [selectedId, setSelectedId] = useState(null);

  // Memoize callback to prevent ListItem re-renders
  const handleDelete = useCallback((id) => {
    // Delete logic
  }, []);

  // Memoize filtered items
  const filteredItems = useMemo(
    () => items.filter(item => item.visible),
    [items]
  );

  return (
    <ul>
      {filteredItems.map(item => (
        <ListItem key={item.id} item={item} onDelete={handleDelete} />
      ))}
    </ul>
  );
}

4. Webpack Bundle Analyzer Optimization

Tool Purpose Installation Insight
webpack-bundle-analyzer Visualize bundle size npm i -D webpack-bundle-analyzer Interactive treemap
source-map-explorer Analyze with source maps npm i -D source-map-explorer See original file sizes
Bundle Buddy Find duplicate code Web tool Identify optimization opportunities
Tree Shaking Remove dead code Built into Webpack 4+ Requires ES6 modules
Code Splitting Split into chunks Webpack config Parallel loading

Example: Bundle analysis and optimization

// Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

// Webpack configuration
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json',
    }),
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

// package.json scripts
{
  "scripts": {
    "build": "webpack --mode production",
    "analyze": "webpack --mode production --profile --json > stats.json && webpack-bundle-analyzer stats.json"
  }
}

// Create React App with analyzer
npm install --save-dev cra-bundle-analyzer

// package.json
{
  "scripts": {
    "analyze": "npm run build && npx cra-bundle-analyzer"
  }
}

// Next.js with bundle analyzer
npm install @next/bundle-analyzer

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Next.js config
});

// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build"
  }
}

// Vite with rollup-plugin-visualizer
npm install --save-dev rollup-plugin-visualizer

// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
};

// Source map explorer
npm install --save-dev source-map-explorer

// package.json
{
  "scripts": {
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  }
}

// Optimization strategies based on bundle analysis

// 1. Replace heavy libraries
// ❌ moment.js (288KB)
import moment from 'moment';
const date = moment().format('YYYY-MM-DD');

// ✅ date-fns (13KB with tree shaking)
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');

// 2. Import only what you need
// ❌ Import entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);

// ✅ Import specific function
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// 3. Use dynamic imports for large libraries
// ❌ Import upfront
import { Chart } from 'chart.js';

// ✅ Load on demand
const loadChart = async () => {
  const { Chart } = await import('chart.js');
  return Chart;
};

// 4. Configure externals (CDN)
// webpack.config.js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};

// HTML
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>

// 5. Optimize images
// Use next/image or similar
import Image from 'next/image';

<Image
  src="/hero.jpg"
  width={800}
  height={600}
  alt="Hero"
  placeholder="blur"
/>

// 6. Remove unused CSS
// Use PurgeCSS or Tailwind's built-in purge
// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  // CSS not used in these files will be removed
};

// 7. Minify and compress
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // Remove console.log
          },
        },
      }),
    ],
  },
  plugins: [
    new CompressionPlugin({
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
    }),
  ],
};

// 8. Analyze and set performance budgets
// webpack.config.js
module.exports = {
  performance: {
    maxAssetSize: 244 * 1024, // 244 KB
    maxEntrypointSize: 244 * 1024,
    hints: 'error', // or 'warning'
  },
};

// Custom bundle size check
const fs = require('fs');
const path = require('path');

const buildFolder = path.join(__dirname, 'build/static/js');
const files = fs.readdirSync(buildFolder);

let totalSize = 0;
files.forEach(file => {
  if (file.endsWith('.js')) {
    const stats = fs.statSync(path.join(buildFolder, file));
    totalSize += stats.size;
  }
});

console.log(`Total JS bundle size: ${(totalSize / 1024).toFixed(2)} KB`);

if (totalSize > 500 * 1024) {
  console.error('Bundle size exceeds 500KB!');
  process.exit(1);
}

5. Image Optimization next/image WebP

Technique Implementation Benefit Browser Support
WebP Format Modern image format 25-35% smaller than JPEG 96%+ (with fallback)
AVIF Format Newest format 50% smaller than JPEG 90%+ (newer browsers)
Lazy Loading loading="lazy" Load images as needed Native browser support
Responsive Images srcset, sizes Load appropriate size Universal support
next/image Next.js Image component Automatic optimization Works everywhere
CDN Image transformation API On-the-fly optimization Cloudinary, Imgix

Example: Modern image optimization techniques

// Next.js Image component (automatic optimization)
import Image from 'next/image';

function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority // Load immediately (above fold)
      placeholder="blur" // Show blur while loading
      blurDataURL="data:image/jpeg;base64,..." // Low-res placeholder
    />
  );
}

// Responsive images with next/image
<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  // Automatically generates srcset
/>

// External images (configure in next.config.js)
// next.config.js
module.exports = {
  images: {
    domains: ['cdn.example.com', 'images.unsplash.com'],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

// Native responsive images with picture element
<picture>
  <source
    type="image/avif"
    srcSet="
      /image-small.avif 400w,
      /image-medium.avif 800w,
      /image-large.avif 1200w
    "
    sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  />
  <source
    type="image/webp"
    srcSet="
      /image-small.webp 400w,
      /image-medium.webp 800w,
      /image-large.webp 1200w
    "
    sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  />
  <img
    src="/image-large.jpg"
    alt="Description"
    loading="lazy"
    decoding="async"
  />
</picture>

// Native lazy loading
<img
  src="image.jpg"
  alt="Description"
  loading="lazy"
  decoding="async"
  width="800"
  height="600"
/>

// Blur placeholder with CSS
.image-container {
  background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);
  position: relative;
}

.image-container::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url('data:image/jpeg;base64,...'); // Tiny base64
  background-size: cover;
  filter: blur(10px);
  transition: opacity 0.3s;
}

.image-container.loaded::before {
  opacity: 0;
}

// Convert images to WebP/AVIF with sharp
npm install sharp

// generateImages.js
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');

async function convertImage(inputPath, outputDir) {
  const filename = path.basename(inputPath, path.extname(inputPath));
  
  // Generate WebP
  await sharp(inputPath)
    .webp({ quality: 80 })
    .toFile(path.join(outputDir, `${filename}.webp`));
  
  // Generate AVIF
  await sharp(inputPath)
    .avif({ quality: 65 })
    .toFile(path.join(outputDir, `${filename}.avif`));
  
  // Generate responsive sizes
  const sizes = [400, 800, 1200];
  for (const size of sizes) {
    await sharp(inputPath)
      .resize(size)
      .webp({ quality: 80 })
      .toFile(path.join(outputDir, `${filename}-${size}.webp`));
  }
}

// Cloudinary integration
<img
  src="https://res.cloudinary.com/demo/image/upload/w_400,f_auto,q_auto/sample.jpg"
  alt="Optimized"
  loading="lazy"
/>
// w_400: width 400px
// f_auto: automatic format (WebP/AVIF)
// q_auto: automatic quality

// React component for optimized images
function OptimizedImage({ src, alt, width, height }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="image-wrapper">
      <picture>
        <source
          type="image/avif"
          srcSet={`${src}.avif`}
        />
        <source
          type="image/webp"
          srcSet={`${src}.webp`}
        />
        <img
          src={`${src}.jpg`}
          alt={alt}
          width={width}
          height={height}
          loading="lazy"
          decoding="async"
          onLoad={() => setLoaded(true)}
          className={loaded ? 'loaded' : ''}
        />
      </picture>
    </div>
  );
}

// Intersection Observer for custom lazy loading
function LazyImage({ src, alt }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { rootMargin: '50px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <img
      ref={imgRef}
      src={isInView ? src : ''}
      alt={alt}
      onLoad={() => setIsLoaded(true)}
      style={{ opacity: isLoaded ? 1 : 0 }}
    />
  );
}

// Progressive JPEG loading
// Use progressive JPEG encoding for better perceived performance
// ImageMagick: convert input.jpg -interlace Plane output.jpg
// Sharp: sharp(input).jpeg({ progressive: true }).toFile(output)

// Image compression before upload
async function compressImage(file) {
  const options = {
    maxSizeMB: 1,
    maxWidthOrHeight: 1920,
    useWebWorker: true,
  };
  
  const compressedFile = await imageCompression(file, options);
  return compressedFile;
}

6. Service Worker Caching Workbox

Strategy Description Use Case Workbox Method
Cache First Cache, fallback to network Static assets (CSS, JS, images) CacheFirst
Network First Network, fallback to cache API calls, dynamic content NetworkFirst
Stale While Revalidate Cache first, update in background Frequent updates (avatars, feeds) StaleWhileRevalidate
Network Only Always use network Real-time data NetworkOnly
Cache Only Only use cache Pre-cached resources CacheOnly
Precaching Cache during install App shell, critical resources precacheAndRoute

Example: Service Worker with Workbox caching strategies

// Install Workbox
npm install workbox-webpack-plugin

// webpack.config.js
const { GenerateSW } = require('workbox-webpack-plugin');

module.exports = {
  plugins: [
    new GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'images',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
            },
          },
        },
        {
          urlPattern: /^https:\/\/api\.example\.com/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            networkTimeoutSeconds: 10,
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 5 * 60, // 5 minutes
            },
          },
        },
      ],
    }),
  ],
};

// Custom service worker with Workbox
// service-worker.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Precache generated assets
precacheAndRoute(self.__WB_MANIFEST);

// Cache images with CacheFirst strategy
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
      }),
    ],
  })
);

// Cache CSS and JavaScript with StaleWhileRevalidate
registerRoute(
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'static-resources',
  })
);

// Cache API responses with NetworkFirst
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 10,
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60, // 5 minutes
      }),
    ],
  })
);

// Cache Google Fonts
registerRoute(
  ({ url }) => url.origin === 'https://fonts.googleapis.com',
  new StaleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets',
  })
);

registerRoute(
  ({ url }) => url.origin === 'https://fonts.gstatic.com',
  new CacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new ExpirationPlugin({
        maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
        maxEntries: 30,
      }),
    ],
  })
);

// Register service worker
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(registration => {
        console.log('SW registered:', registration);
      })
      .catch(error => {
        console.log('SW registration failed:', error);
      });
  });
}

// Create React App with Workbox
// src/service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';

clientsClaim();

precacheAndRoute(self.__WB_MANIFEST);

const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
registerRoute(
  ({ request, url }) => {
    if (request.mode !== 'navigate') return false;
    if (url.pathname.startsWith('/_')) return false;
    if (url.pathname.match(fileExtensionRegexp)) return false;
    return true;
  },
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);

registerRoute(
  ({ url }) =>
    url.origin === self.location.origin && url.pathname.endsWith('.png'),
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [new ExpirationPlugin({ maxEntries: 50 })],
  })
);

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

// Register in index.js
import * as serviceWorkerRegistration from './serviceWorkerRegistration';

serviceWorkerRegistration.register();

// Next.js with next-pwa
npm install next-pwa

// next.config.js
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'google-fonts',
        expiration: {
          maxEntries: 4,
          maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
        },
      },
    },
    {
      urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'static-font-assets',
        expiration: {
          maxEntries: 4,
          maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
        },
      },
    },
    {
      urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'static-image-assets',
        expiration: {
          maxEntries: 64,
          maxAgeSeconds: 24 * 60 * 60, // 24 hours
        },
      },
    },
  ],
});

module.exports = withPWA({
  // Next.js config
});

// Offline fallback page
// service-worker.js
import { offlineFallback } from 'workbox-recipes';

offlineFallback({
  pageFallback: '/offline.html',
});

// Background sync for failed requests
import { BackgroundSyncPlugin } from 'workbox-background-sync';

const bgSyncPlugin = new BackgroundSyncPlugin('api-queue', {
  maxRetentionTime: 24 * 60, // Retry for up to 24 hours (in minutes)
});

registerRoute(
  /\/api\/.*\/*.json/,
  new NetworkOnly({
    plugins: [bgSyncPlugin],
  }),
  'POST'
);

// Cache analytics requests
import { Queue } from 'workbox-background-sync';

const queue = new Queue('analytics-queue');

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/analytics')) {
    const promiseChain = fetch(event.request.clone()).catch(() => {
      return queue.pushRequest({ request: event.request });
    });

    event.waitUntil(promiseChain);
  }
});

Performance Optimization Summary

  • Core Web Vitals - Measure LCP (<2.5s), CLS (<0.1), FID/INP (<100ms/200ms) using web-vitals library, optimize with preload, proper dimensions, code splitting
  • Code Splitting - Use React.lazy + Suspense for route/component splitting, webpack magic comments for chunk naming, prefetch/preload for predictive loading
  • Memoization - React.memo for expensive component renders, useMemo for computed values, useCallback for function references passed to children
  • Bundle Analysis - Use webpack-bundle-analyzer or rollup visualizer, identify large dependencies, replace heavy libraries, tree shake unused code
  • Image Optimization - Use WebP/AVIF formats, next/image for automatic optimization, responsive images with srcset, lazy loading, blur placeholders
  • Service Workers - Implement caching strategies with Workbox: CacheFirst for static assets, NetworkFirst for API, StaleWhileRevalidate for frequently updated content
  • Best Practices - Set performance budgets, monitor with Lighthouse CI, compress assets (gzip/brotli), use CDN, minimize main thread work