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.
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
- Understanding Website Performance Metrics
- Core Web Vitals Optimization
- Server-Level Optimizations
- Caching Strategies
- Content Delivery Networks
- Image and Media Optimization
- Database Performance Tuning
- Frontend Optimization Techniques
- 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?
- Compare High-Performance Hosting - Find fast hosting providers
- VPS Hosting Guide - Optimize your VPS
- Dedicated Server Guide - Server performance tuning
Last updated: January 20, 2025 Difficulty: Intermediate Prerequisites: Basic web development, server administration knowledge
Related Guides
Dedicated Server Complete Guide: Setup, Security & Management
Complete guide to dedicated server hosting. Learn how to choose, configure, secure, and manage dedicated servers for high-traffic websites and applications.
Complete Guide to VPS Hosting
Everything you need to know about Virtual Private Servers, from basics to advanced configurations. Learn when to use VPS, how to choose specs, and best practices.