Back to Guides
Intermediate

Website Performance Optimization: Complete 2025 Guide

Learn how to optimize website performance for speed and Core Web Vitals. Covers caching, CDN, image optimization, database tuning, and server configuration.

ComparisonHost Team
13 min read
#performance optimization#website speed#core web vitals#caching#cdn
Share:

Website performance directly impacts user experience, search rankings, and conversions. A one-second delay in page load time can reduce conversions by seven percent, and 53 percent of mobile users abandon sites that take over three seconds to load.

This comprehensive guide covers proven techniques to optimize website performance, improve Core Web Vitals scores, and deliver fast loading times across all devices.

Table of Contents

  1. Understanding Website Performance Metrics
  2. Core Web Vitals Optimization
  3. Server-Level Optimizations
  4. Caching Strategies
  5. Content Delivery Networks
  6. Image and Media Optimization
  7. Database Performance Tuning
  8. Frontend Optimization Techniques
  9. Monitoring and Testing Tools

Understanding Website Performance Metrics

Key Performance Indicators

Time to First Byte (TTFB)

  • Measures server response time
  • Target: Under 200ms (excellent), under 600ms (acceptable)
  • Affected by: Server processing, database queries, network latency

First Contentful Paint (FCP)

  • When first content appears on screen
  • Target: Under 1.8 seconds (good), under 3 seconds (needs improvement)
  • Affected by: Render-blocking resources, server response time

Largest Contentful Paint (LCP)

  • When main content is fully visible
  • Target: Under 2.5 seconds (good), under 4 seconds (needs improvement)
  • Google Core Web Vital metric
  • Affected by: Image size, server response, render-blocking resources

First Input Delay (FID)

  • Time from user interaction to browser response
  • Target: Under 100ms (good), under 300ms (needs improvement)
  • Google Core Web Vital metric
  • Affected by: JavaScript execution, main thread blocking

Cumulative Layout Shift (CLS)

  • Visual stability during page load
  • Target: Under 0.1 (good), under 0.25 (needs improvement)
  • Google Core Web Vital metric
  • Affected by: Images without dimensions, dynamic content injection

Total Blocking Time (TBT)

  • Time when main thread is blocked
  • Target: Under 200ms (good), under 600ms (needs improvement)
  • Affected by: Large JavaScript bundles, unoptimized code

Performance Budget Guidelines

Page Weight Targets:

  • Total page size: Under 1.5MB (ideal), under 3MB (acceptable)
  • HTML: Under 50KB
  • CSS: Under 100KB
  • JavaScript: Under 300KB
  • Images: Under 1MB total
  • Fonts: Under 100KB

Request Count Targets:

  • Total requests: Under 50 (ideal), under 100 (acceptable)
  • Third-party requests: Under 10
  • Font requests: 2-4 maximum

Core Web Vitals Optimization

Improving Largest Contentful Paint LCP

Optimize Server Response Time

# Nginx configuration for fast TTFB
server {
    # Enable HTTP/2
    listen 443 ssl http2;

    # Enable gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript
               application/javascript application/xml+rss application/json;

    # Browser caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Optimize Images for LCP

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

<!-- Use responsive images -->
<img
  src="/hero-1200.webp"
  srcset="/hero-400.webp 400w,
          /hero-800.webp 800w,
          /hero-1200.webp 1200w"
  sizes="(max-width: 600px) 400px,
         (max-width: 900px) 800px,
         1200px"
  alt="Hero image"
  loading="eager"
  decoding="async"
  width="1200"
  height="600"
>

Remove Render-Blocking Resources

<!-- Defer non-critical CSS -->
<link rel="preload" href="/critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/critical.css"></noscript>

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

<!-- Inline critical CSS -->
<style>
  /* Critical above-the-fold styles */
  body { margin: 0; font-family: sans-serif; }
  .header { background: #333; color: white; }
</style>

Reducing First Input Delay FID

Code Splitting and Lazy Loading

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

// Lazy load heavy components
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false
});

// React lazy loading
const VideoPlayer = React.lazy(() => import('./VideoPlayer'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <VideoPlayer />
    </Suspense>
  );
}

Web Worker for Heavy Processing

// 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);
});

Debounce and Throttle User Interactions

// Debounce search input
const debounce = (func, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
};

const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce((e) => {
  performSearch(e.target.value);
}, 300));

// Throttle scroll events
const throttle = (func, limit) => {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
};

window.addEventListener('scroll', throttle(() => {
  updateScrollPosition();
}, 100));

Minimizing Cumulative Layout Shift CLS

Set Explicit Dimensions

<!-- Always specify width and height -->
<img src="/product.jpg" width="400" height="300" alt="Product">

<!-- CSS aspect ratio for responsive images -->
<style>
  .image-container {
    aspect-ratio: 16 / 9;
    width: 100%;
  }
  .image-container img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
</style>

Reserve Space for Dynamic Content

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

/* Skeleton loading for content */
.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

Font Loading Strategy

/* Prevent layout shift from font loading */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap;
  size-adjust: 95%; /* Match fallback font metrics */
}

body {
  font-family: 'CustomFont', Arial, sans-serif;
}

Server-Level Optimizations

Enable HTTP/2 or HTTP/3

Nginx HTTP/2 Configuration

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # SSL optimization
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
}

Apache HTTP/2 Configuration

# Enable HTTP/2 module
LoadModule http2_module modules/mod_http2.so

<VirtualHost *:443>
    ServerName example.com

    Protocols h2 http/1.1

    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
</VirtualHost>

Enable Compression

Gzip Configuration

# Nginx gzip
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
  text/plain
  text/css
  text/xml
  text/javascript
  application/javascript
  application/xml+rss
  application/json
  image/svg+xml;

Brotli Compression (Better than Gzip)

# Install nginx-module-brotli first
brotli on;
brotli_comp_level 6;
brotli_types
  text/plain
  text/css
  text/xml
  text/javascript
  application/javascript
  application/json
  application/xml+rss
  image/svg+xml;

PHP-FPM Optimization

# /etc/php/8.2/fpm/pool.d/www.conf

[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

# Enable OPcache
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.fast_shutdown=1

Caching Strategies

Browser Caching

Cache-Control Headers

# Static assets - cache for 1 year
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# CSS and JavaScript - cache but allow revalidation
location ~* \.(css|js)$ {
    expires 1M;
    add_header Cache-Control "public, must-revalidate";
}

# HTML - no cache
location ~* \.(html)$ {
    expires -1;
    add_header Cache-Control "no-store, no-cache, must-revalidate";
}

Server-Side Caching with Redis

PHP Redis Caching

// Connect to Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// Cache database query results
$cacheKey = 'products:featured';
$cachedData = $redis->get($cacheKey);

if ($cachedData) {
    $products = json_decode($cachedData, true);
} else {
    // Query database
    $products = $db->query("SELECT * FROM products WHERE featured = 1");

    // Cache for 1 hour
    $redis->setex($cacheKey, 3600, json_encode($products));
}

Node.js Redis Caching

const redis = require('redis');
const client = redis.createClient();

async function getCachedData(key, fetchFunction, ttl = 3600) {
    // Try to get from cache
    const cached = await client.get(key);
    if (cached) {
        return JSON.parse(cached);
    }

    // Fetch fresh data
    const data = await fetchFunction();

    // Store in cache
    await client.setex(key, ttl, JSON.stringify(data));

    return data;
}

// Usage
const products = await getCachedData('products:all', async () => {
    return await db.query('SELECT * FROM products');
}, 3600);

Full Page Caching with Varnish

Varnish VCL Configuration

vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Don't cache admin pages
    if (req.url ~ "^/admin" || req.url ~ "^/wp-admin") {
        return (pass);
    }

    # Don't cache if user is logged in
    if (req.http.Cookie ~ "wordpress_logged_in|wp-postpass") {
        return (pass);
    }

    # Remove cookies for static files
    if (req.url ~ "\.(jpg|jpeg|png|gif|css|js|ico)$") {
        unset req.http.Cookie;
    }
}

sub vcl_backend_response {
    # Cache static files for 1 day
    if (bereq.url ~ "\.(jpg|jpeg|png|gif|css|js|ico)$") {
        set beresp.ttl = 1d;
    }

    # Cache HTML for 5 minutes
    if (beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 5m;
    }
}

Content Delivery Networks

CDN Configuration Best Practices

Cloudflare Page Rules

Page Rule 1: Cache Everything
URL: example.com/*
Settings:
  - Cache Level: Cache Everything
  - Edge Cache TTL: 1 month
  - Browser Cache TTL: 4 hours

Page Rule 2: Bypass Cache for Admin
URL: example.com/admin/*
Settings:
  - Cache Level: Bypass

Page Rule 3: Optimize Images
URL: example.com/*
Settings:
  - Auto Minify: JavaScript, CSS, HTML
  - Rocket Loader: On
  - Polish: Lossless

CloudFront Cache Behaviors

# CloudFront distribution config
CacheBehaviors:
  - PathPattern: "/static/*"
    TargetOriginId: S3Origin
    ViewerProtocolPolicy: redirect-to-https
    Compress: true
    CachePolicyId: CachingOptimized

  - PathPattern: "/api/*"
    TargetOriginId: ALBOrigin
    ViewerProtocolPolicy: https-only
    CachePolicyId: CachingDisabled

  - PathPattern: "*.jpg"
    TargetOriginId: S3Origin
    ViewerProtocolPolicy: redirect-to-https
    Compress: true
    CachePolicyId: CachingOptimized

Self-Hosted CDN with Multiple Servers

Nginx Multi-Origin Configuration

upstream cdn_origin {
    server cdn1.example.com:80 weight=3;
    server cdn2.example.com:80 weight=2;
    server cdn3.example.com:80 weight=1;
}

server {
    listen 80;
    server_name cdn.example.com;

    location / {
        proxy_pass http://cdn_origin;
        proxy_cache cdn_cache;
        proxy_cache_valid 200 1d;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Image and Media Optimization

Modern Image Formats

WebP Conversion

# Convert JPEG/PNG to WebP
cwebp input.jpg -q 80 -o output.webp

# Batch convert
for img in *.jpg; do
  cwebp "$img" -q 80 -o "${img%.jpg}.webp"
done

AVIF Format (Next-Gen)

# Convert to AVIF (better compression than WebP)
avifenc --min 0 --max 63 -a end-usage=q -a cq-level=25 input.jpg output.avif

Picture Element with Fallbacks

<picture>
  <source srcset="/image.avif" type="image/avif">
  <source srcset="/image.webp" type="image/webp">
  <img src="/image.jpg" alt="Optimized image" width="800" height="600">
</picture>

Lazy Loading Images

<!-- Native lazy loading -->
<img src="/image.jpg" loading="lazy" alt="Lazy loaded image">

<!-- Intersection Observer for custom lazy loading -->
<script>
  const images = document.querySelectorAll('img[data-src]');

  const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        observer.unobserve(img);
      }
    });
  });

  images.forEach(img => imageObserver.observe(img));
</script>

Video Optimization

Responsive Video Poster

<video width="1280" height="720" poster="/video-poster.webp" preload="metadata">
  <source src="/video-720p.mp4" type="video/mp4" media="(max-width: 640px)">
  <source src="/video-1080p.mp4" type="video/mp4" media="(min-width: 641px)">
</video>

Lazy Load Videos

const videos = document.querySelectorAll('video[data-src]');

const videoObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const video = entry.target;
      const sources = video.querySelectorAll('source');

      sources.forEach(source => {
        source.src = source.dataset.src;
      });

      video.load();
      videoObserver.unobserve(video);
    }
  });
});

videos.forEach(video => videoObserver.observe(video));

Database Performance Tuning

MySQL/MariaDB Optimization

Configuration Tuning

# /etc/mysql/mariadb.conf.d/50-server.cnf

[mysqld]
# InnoDB settings (70% of available RAM)
innodb_buffer_pool_size = 8G
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT

# Query cache
query_cache_type = 1
query_cache_size = 256M
query_cache_limit = 2M

# Connection settings
max_connections = 500
max_allowed_packet = 256M
wait_timeout = 600

# Table cache
table_open_cache = 4000
table_definition_cache = 2000

Index Optimization

-- Find missing indexes
SELECT
    CONCAT(table_schema, '.', table_name) AS 'Table',
    ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.TABLES
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema')
GROUP BY table_schema, table_name
ORDER BY SUM(data_length + index_length) DESC;

-- Add composite index
CREATE INDEX idx_user_created ON users(status, created_at);

-- Analyze slow queries
SHOW FULL PROCESSLIST;
SHOW STATUS LIKE 'Slow_queries';

Query Optimization

Use EXPLAIN to Analyze Queries

-- Check query execution plan
EXPLAIN SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
  AND o.created_at > '2024-01-01';

-- Optimize with proper indexes
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_created ON orders(created_at);
CREATE INDEX idx_orders_user ON orders(user_id);

Pagination with OFFSET Alternative

-- Slow: OFFSET 10000
SELECT * FROM posts ORDER BY id DESC LIMIT 20 OFFSET 10000;

-- Fast: Keyset pagination
SELECT * FROM posts WHERE id < 10000 ORDER BY id DESC LIMIT 20;

Connection Pooling

PHP PDO Connection Pool

class DatabasePool {
    private static $pool = [];
    private static $maxConnections = 10;

    public static function getConnection() {
        if (count(self::$pool) > 0) {
            return array_pop(self::$pool);
        }

        $dsn = "mysql:host=localhost;dbname=mydb";
        return new PDO($dsn, 'user', 'password', [
            PDO::ATTR_PERSISTENT => true,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]);
    }

    public static function releaseConnection($conn) {
        if (count(self::$pool) < self::$maxConnections) {
            self::$pool[] = $conn;
        }
    }
}

Frontend Optimization Techniques

Minification and Bundling

Webpack Production Config

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            pure_funcs: ['console.log']
          }
        }
      }),
      new CssMinimizerPlugin()
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        }
      }
    }
  }
};

Critical CSS Extraction

// Using critical npm package
const critical = require('critical');

critical.generate({
  inline: true,
  base: 'dist/',
  src: 'index.html',
  dest: 'index-critical.html',
  width: 1300,
  height: 900,
  minify: true
});

Resource Hints

<!-- DNS Prefetch -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//cdn.example.com">

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

<!-- Prefetch (low priority) -->
<link rel="prefetch" href="/next-page.html">

<!-- Preload (high priority) -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero.webp" as="image">

Monitoring and Testing Tools

Performance Testing Tools

Lighthouse CI Integration

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://example.com
            https://example.com/about
          budgetPath: ./budget.json
          uploadArtifacts: true

WebPageTest API

const WebPageTest = require('webpagetest');
const wpt = new WebPageTest('www.webpagetest.org', 'YOUR_API_KEY');

wpt.runTest('https://example.com', {
  location: 'Dulles:Chrome',
  connectivity: '4G',
  runs: 3,
  firstViewOnly: false
}, (err, result) => {
  console.log('Test ID:', result.data.testId);
  console.log('Results:', result.data.summary);
});

Real User Monitoring

Google Analytics 4 Web Vitals

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

function sendToAnalytics(metric) {
  gtag('event', metric.name, {
    value: Math.round(metric.value),
    event_category: 'Web Vitals',
    event_label: metric.id,
    non_interaction: true
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Performance Checklist

Server Configuration:

  • Enable HTTP/2 or HTTP/3
  • Enable Brotli or Gzip compression
  • Configure proper caching headers
  • Optimize PHP-FPM or application server
  • Enable OPcache for PHP

Database Optimization:

  • Add indexes to frequently queried columns
  • Enable query caching
  • Use connection pooling
  • Optimize slow queries with EXPLAIN

Frontend Optimization:

  • Minify CSS, JavaScript, and HTML
  • Implement code splitting
  • Use lazy loading for images and videos
  • Defer non-critical JavaScript
  • Inline critical CSS

Image Optimization:

  • Convert to WebP or AVIF
  • Use responsive images with srcset
  • Implement lazy loading
  • Compress images (80-85 quality)
  • Set explicit width and height

Caching Strategy:

  • Browser caching for static assets
  • Server-side caching with Redis
  • Full-page caching with Varnish
  • CDN for global distribution

Monitoring:

  • Set up Lighthouse CI
  • Monitor Core Web Vitals
  • Track real user metrics
  • Regular performance audits

Conclusion

Website performance optimization is an ongoing process. Key takeaways:

  • Prioritize Core Web Vitals (LCP, FID, CLS) for SEO and user experience
  • Implement multi-layer caching (browser, server, CDN)
  • Optimize images and use modern formats (WebP, AVIF)
  • Minimize and defer JavaScript execution
  • Monitor performance continuously with real user metrics

Start with quick wins (image optimization, caching) then move to advanced techniques (server tuning, code splitting). Test every change and measure impact with real data.

Ready to optimize your hosting environment?


Last updated: January 20, 2025 Difficulty: Intermediate Prerequisites: Basic web development, server administration knowledge

Share: