How Web Performance Impacts Business
A 1-second delay in page loading reduces conversions by 7%. Web performance is not a technical issue — it is a business issue. If you walk into a restaurant and have to wait 30 minutes after ordering, most customers leave. Websites are no different.
Google has factored Core Web Vitals into search rankings since 2021. Sites with better performance also have an SEO advantage.
Understanding Core Web Vitals
| Metric | What it measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Render time of the largest content | 2.5s or less | 2.5-4s | Over 4s |
| INP (Interaction to Next Paint) | User interaction response time | 200ms or less | 200-500ms | Over 500ms |
| CLS (Cumulative Layout Shift) | Visual stability (layout movement) | 0.1 or less | 0.1-0.25 | Over 0.25 |
Measurement Tools
# Measure performance with Lighthouse CLI
npm install -g lighthouse
lighthouse https://example.com --output html --output-path ./report.html
# Using PageSpeed Insights API
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?\
url=https://example.com&\
category=performance&\
strategy=mobile"
Improving LCP: Render the Largest Content Fast
LCP measures the time until the largest image or text block in the viewport is rendered.
Key Causes and Solutions
<!-- 1. Optimize hero images — load the LCP element fast -->
<!-- Bad: lazy loading applied to LCP element -->
<img src="hero.jpg" loading="lazy" alt="Main banner" />
<!-- Good: LCP element loads immediately + preload -->
<link rel="preload" as="image" href="hero.webp" />
<img src="hero.webp"
fetchpriority="high"
alt="Main banner"
width="1200"
height="600" />
<!-- 2. Inline critical CSS -->
<head>
<!-- Inline CSS needed for the first screen -->
<style>
/* Critical CSS — only styles essential for initial render */
.hero { display: flex; align-items: center; min-height: 60vh; }
.hero-title { font-size: 2.5rem; font-weight: 700; }
.nav { display: flex; gap: 1rem; padding: 1rem; }
</style>
<!-- Load remaining CSS asynchronously -->
<link rel="preload" as="style" href="/styles/main.css"
onload="this.onload=null;this.rel='stylesheet'" />
</head>
// 3. Optimize server response time (improve TTFB)
// Response compression + cache headers in Express.js
import express from "express";
import compression from "compression";
const app = express();
// Enable Gzip/Brotli compression
app.use(compression({
level: 6, // Compression level (1-9, 6 is balanced)
threshold: 1024 // Only compress files over 1KB
}));
// Set cache headers for static files
app.use("/static", express.static("public", {
maxAge: "30d", // 30-day cache
immutable: true, // When filenames include a hash
etag: true
}));
// Always serve the latest HTML version
app.use((req, res, next) => {
if (req.path.endsWith(".html") || !req.path.includes(".")) {
res.setHeader("Cache-Control", "no-cache, must-revalidate");
}
next();
});
Improving CLS: Preventing Layout Shifts
CLS measures when elements suddenly shift during page loading. The experience of a button moving just as you try to click it is extremely frustrating.
<!-- 1. Specify dimensions for images/videos — reserve space in advance -->
<!-- Bad: no dimensions → layout shifts when image loads -->
<img src="photo.webp" alt="Photo" />
<!-- Good: set width/height or aspect-ratio -->
<img src="photo.webp" alt="Photo" width="800" height="600" />
<!-- Using CSS aspect-ratio -->
<style>
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
background: #f0f0f0; /* Placeholder during loading */
}
</style>
/* 2. Prevent layout shifts from web font loading */
/* font-display: swap — show system font before custom font loads */
@font-face {
font-family: "Pretendard";
src: url("/fonts/Pretendard.woff2") format("woff2");
font-display: swap; /* Allow FOUT, prevent CLS */
size-adjust: 100%; /* Minimize size difference on font swap */
}
/* 3. Reserve space for dynamic content in advance */
.ad-slot {
min-height: 250px; /* Reserve ad area in advance */
background: #f5f5f5;
contain: layout; /* Isolate with CSS containment */
}
.skeleton-card {
height: 200px; /* Reserve space with skeleton UI */
border-radius: 8px;
background: linear-gradient(
90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%
);
animation: shimmer 1.5s infinite;
}
Image Optimization
Images account for more than 50% of the total page weight on average.
<!-- 1. Use next-gen formats + fallback -->
<picture>
<!-- AVIF — best compression (Chrome 85+, Firefox 93+) -->
<source type="image/avif" srcset="photo.avif" />
<!-- WebP — wide compatibility (all except IE) -->
<source type="image/webp" srcset="photo.webp" />
<!-- JPEG — fallback -->
<img src="photo.jpg" alt="Photo" width="800" height="600"
loading="lazy" decoding="async" />
</picture>
<!-- 2. Responsive images — serve the right image for screen size -->
<img srcset="photo-400w.webp 400w,
photo-800w.webp 800w,
photo-1200w.webp 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1024px) 800px,
1200px"
src="photo-800w.webp"
alt="Responsive image"
loading="lazy"
decoding="async" />
| Format | Compression (vs JPEG) | Browser support | Use case |
|---|---|---|---|
| AVIF | 50% smaller | Modern browsers | Photos, illustrations |
| WebP | 30% smaller | All except IE | General purpose |
| JPEG | Baseline | All | Fallback |
| PNG | Larger | All | When transparency is needed |
| SVG | Very small | All | Icons, logos |
Browser Caching Strategy
# Nginx caching configuration example
# Static assets — long-term cache (assumes hashed filenames)
location ~* \.(js|css|woff2|avif|webp|png|jpg|svg)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# HTML — always revalidate
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# API responses — no caching
location /api/ {
add_header Cache-Control "no-store";
}
| Strategy | Cache-Control | Target | Description |
|---|---|---|---|
| Long-term cache | max-age=31536000, immutable | JS, CSS, images (with hash) | Invalidate cache by changing filename |
| Revalidate | no-cache | HTML | Check with server before each use |
| No cache | no-store | API, personal data | Never cache |
| Short cache | max-age=300 | Frequently changing data | Cache for 5 minutes |
Practical Tips
- Measure first, optimize later: Do not optimize by intuition. Use Lighthouse and WebPageTest to pinpoint bottlenecks before making improvements.
- Apply the highest-impact changes first: Image optimization (WebP conversion) and caching configuration alone can dramatically improve perceived performance.
- Monitor bundle size: Use
webpack-bundle-analyzerorsource-map-explorerto inspect bundle composition. Unused libraries may be included. - Manage third-party scripts: Analytics, ads, and chat widgets are often the primary culprits for performance degradation. Apply
deferorasync. - Set a Performance Budget: Define budgets like “JS bundle under 300KB” and “LCP under 2 seconds”, then automatically check them in CI.