Skip to main content
โšก Calmops

Frontend Performance Optimization: Complete Guide

Frontend performance is critical for user experience and SEO. This guide covers comprehensive optimization techniques for modern web applications.

Core Web Vitals

LCP (Largest Contentful Paint)

Measures loading performance - when is the largest content visible?

<!-- Optimize LCP -->
<!-- Preload hero image -->
<link rel="preload" as="image" href="hero.webp">

<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

<!-- Inline critical CSS -->
<style>
  /* Critical CSS only */
  header { ... }
  .hero { ... }
</style>

FID (First Input Delay)

Measures interactivity - how quickly can users interact?

// Defer non-critical JavaScript
<script defer src="analytics.js"></script>
<script async src="ads.js"></script>

// Break up long tasks
function processItems(items) {
  const chunkSize = 10;
  
  function processChunk(start) {
    const end = Math.min(start + chunkSize, items.length);
    
    for (let i = start; i < end; i++) {
      processItem(items[i]);
    }
    
    if (end < items.length) {
      // Schedule next chunk
      setTimeout(() => processChunk(end), 0);
    }
  }
  
  processChunk(0);
}

CLS (Cumulative Layout Shift)

Measures visual stability - does content shift unexpectedly?

/* Reserve space for images */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* Reserve space for ads/banners */
.ad-banner {
  min-height: 250px;
  background: #f0f0f0;
}

/* Use font-display: optional or swap */
@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: swap;
}

Code Splitting

Dynamic Imports

// Instead of
import { HeavyComponent } from './HeavyComponent';

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

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

Route-Based Splitting

// React Router
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />} />
      <Route path="/settings" element={<Settings />} />
      <Route path="/profile" element={<Profile />} />
    </Routes>
  );
}

Webpack Code Splitting

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        }
      }
    }
  }
};

Image Optimization

Responsive Images

<img 
  srcset="image-400.jpg 400w,
          image-800.jpg 800w,
          image-1200.jpg 1200w"
  sizes="(max-width: 600px) 400px,
         (max-width: 1200px) 800px,
         1200px"
  src="image-800.jpg"
  alt="Description"
  loading="lazy"
  decoding="async"
>

Modern Formats

<!-- Use WebP with fallbacks -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description" loading="lazy">
</picture>

Lazy Loading

// Native lazy loading
<img src="image.jpg" loading="lazy" alt="...">

// Intersection Observer for background images
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const el = entry.target;
      el.style.backgroundImage = `url(${el.dataset.src})`;
      observer.unobserve(el);
    }
  });
});

document.querySelectorAll('[data-src]').forEach(el => {
  observer.observe(el);
});

JavaScript Optimization

Tree Shaking

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
  }
};

// Use ES modules - webpack will tree shake
import { cloneDeep, debounce } from 'lodash';

// Bad - imports everything
import _ from 'lodash';

// Good - named imports
import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep';

// Better - use specific libraries
import debounce from 'debounce';
import cloneDeep from 'lodash-es/cloneDeep';

Compression

// Gzip or Brotli
// Server configuration (nginx)
gzip on;
gzip_types text/plain text/css application/json 
  application/javascript text/xml application/xml;

// Brotli (better compression)
brotli on;
brotli_types text/plain text/css application/json 
  application/javascript text/xml;

Minification

// webpack production build automatically minifies
// terser for JS, css-minimizer for CSS

// package.json
{
  "scripts": {
    "build": "webpack --mode production",
    "build:analyze": "webpack --mode production --analyze"
  }
}

CSS Optimization

Critical CSS

<!-- Inline critical CSS in <head> -->
<head>
  <style>
    /* Only styles needed for above-the-fold content */
    header { background: white; }
    .hero { min-height: 100vh; }
    .nav { display: flex; }
  </style>
  
  <!-- Defer non-critical CSS -->
  <link rel="preload" href="styles.css" as="style"
    onload="this.onload=null;this.rel='stylesheet'">
  <noscript>
    <link rel="stylesheet" href="styles.css">
  </noscript>
</head>

Remove Unused CSS

// webpack.config.js with purgecss
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');

module.exports = {
  plugins: [
    new PurgeCSSPlugin({
      paths: glob.sync([
        path.join(__dirname, 'src/**/*'),
        path.join(__dirname, 'public/**/*.html'),
      ]),
      safelist: {
        standard: [/^modal/, /^nav-/],
      }
    })
  ]
};

Caching Strategies

Service Worker Caching

// sw.js
const CACHE_NAME = 'v1';
const urlsToCache = ['/', '/index.html', '/styles.css', '/script.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cache or fetch
        return response || fetch(event.request);
      })
  );
});

// Cache-first for static assets
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/static/')) {
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetch(event.request))
    );
  }
});

HTTP Caching

# .htaccess / Apache
<IfModule mod_expires.c>
  ExpiresActive On
  
  # Images - 1 year
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
  
  # CSS/JS - 1 month
  ExpiresByType text/css "access plus 1 month"
  ExpiresByType application/javascript "access plus 1 month"
  
  # HTML - no cache
  ExpiresByType text/html "access plus 0 seconds"
</IfModule>

Bundle Size Analysis

Source Map Explorer

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

// Usage
{
  "scripts": {
    "analyze": "source-map-explorer build/**/*.js"
  }
}

// webpack configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

Performance Budgets

// webpack.config.js
module.exports = {
  performance: {
    maxEntrypointSize: 512000,
    maxAssetSize: 512000,
    hints: 'warning'
  }
};

// Or in package.json
{
  "name": "my-app",
  "size-limit": [
    {
      "path": "dist/**/*.js",
      "limit": "500 kB"
    }
  ]
}

Performance Monitoring

Web Vitals Library

import { getCLS, getFID, getLCP } from 'web-vitals';

function sendToAnalytics({ name, delta, id }) {
  ga('send', 'event', {
    eventCategory: 'Web Vitals',
    eventAction: name,
    eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
    eventLabel: id,
    nonInteraction: true,
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

React Profiler

function App() {
  const handleRender = (id, phase, actualDuration) => {
    console.log(`${id} ${phase}: ${actualDuration}ms`);
  };
  
  return (
    <Profiler id="App" onRender={handleRender}>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
        </Routes>
      </Router>
    </Profiler>
  );
}

Quick Wins

<!-- 1. Preconnect to origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 2. Prefetch critical resources -->
<link rel="prefetch" href="/page.js">

<!-- 3. DNS prefetch -->
<link rel="dns-prefetch" href="https://api.example.com">

<!-- 4. Defer JavaScript -->
<script src="app.js" defer></script>

<!-- 5. Async JavaScript -->
<script src="analytics.js" async></script>

Summary

Key performance optimizations:

  1. Core Web Vitals

    • LCP: Preload critical resources
    • FID: Defer JavaScript
    • CLS: Reserve space, stable fonts
  2. Code Splitting

    • Route-based splitting
    • Dynamic imports
    • Tree shaking
  3. Images

    • Responsive images
    • Modern formats (WebP, AVIF)
    • Lazy loading
  4. Caching

    • Service workers
    • HTTP caching
    • CDN
  5. Monitoring

    • Web Vitals
    • Real user monitoring
    • Performance budgets

Optimize early and monitor continuously!

Comments