Speed Index is the cinematographer of web performance metrics—it doesn't just capture a single moment but the entire visual story of how your page loads. By measuring the progressive visual completeness of a page over time, Speed Index provides a nuanced view of perceived performance that single-point metrics like FCP or LCP miss. It answers the critical question: "How fast does the page look like it's loading?"
What is Speed Index?
Speed Index measures the average time at which visible parts of the page are displayed during load. It's calculated by capturing video of the page loading and computing the visual progression between frames. The result is expressed in milliseconds, with lower values indicating faster visual loading.
The calculation works by:
- Recording page load as video (typically at 10fps)
- Calculating visual completeness percentage for each frame
- Computing the area above the visual progress curve
- Expressing result in milliseconds
Mathematical formula:
Speed Index = Σ (1 - VC(t)) * interval
Where VC(t) is visual completeness at time t
Lighthouse Speed Index thresholds:
- Good: 3.4 seconds or less
- Needs Improvement: 3.4 to 5.8 seconds
- Poor: Over 5.8 seconds
Why Speed Index Matters
Beyond Single-Point Metrics
Traditional metrics capture specific moments:
- FCP: When first content appears
- LCP: When largest content appears
- TTI: When page becomes interactive
Speed Index captures the entire journey:
Visual Progress Comparison:
Site A (FCP=1s, LCP=3s, SI=2s):
100% ████████████████████
75% ████████████
50% ██████
25% ███
0% |-------|-------|
0 2s 4s
Site B (FCP=0.5s, LCP=3s, SI=3.5s):
100% ████████████████████
75% ████
50% ██
25% ██
0% |-------|-------|
0 2s 4s
Site A has better Speed Index despite slower FCP
User Perception Correlation
Speed Index strongly correlates with user satisfaction because it measures what users actually see:
- Visual feedback loop: Users perceive progress when pixels change
- Psychological anchoring: Early visual progress sets expectations
- Completion perception: Gradual filling feels faster than sudden appearance
Studies show:
- 1-second Speed Index improvement increases engagement by 5%
- Sites with SI under 3s have 20% lower bounce rates
- Progressive rendering (good SI) improves perceived performance by 30%
Business Impact
Companies optimizing Speed Index report:
Zillow: Reduced SI by 1.2s
- 7% increase in conversion rate
- 4% reduction in bounce rate
Pinterest: Improved SI by 40%
- 15% increase in SEO traffic
- 10% increase in signup conversion
Financial Times: Achieved 2.8s Speed Index
- 23% increase in user engagement
- 7% increase in article reads
How Speed Index Works
Visual Completeness Calculation
Speed Index uses computer vision to determine visual completeness:
// Simplified visual completeness algorithm
function calculateVisualCompleteness(frame, finalFrame) {
let matchingPixels = 0
let totalPixels = frame.width * frame.height
for (let i = 0; i < totalPixels; i++) {
if (frame.pixels[i] === finalFrame.pixels[i]) {
matchingPixels++
}
}
return matchingPixels / totalPixels // 0 to 1
}
// Calculate Speed Index
function calculateSpeedIndex(frames, interval = 100) {
let speedIndex = 0
let lastCompleteness = 0
frames.forEach((frame, index) => {
const completeness = calculateVisualCompleteness(
frame,
frames[frames.length - 1]
)
const incomplete = 1 - (lastCompleteness + completeness) / 2
speedIndex += incomplete * interval
lastCompleteness = completeness
})
return speedIndex
}
Viewport Considerations
Speed Index only measures the visible viewport:
- Content below the fold doesn't affect SI
- Mobile viewport typically 360x640px
- Desktop viewport typically 1366x768px
- Responsive design impacts SI differently per breakpoint
Perceptual Speed Index
A variant that considers human visual perception:
// Perceptual Speed Index weights changes by visual importance
function perceptualSpeedIndex(frames) {
// Apply SSIM (Structural Similarity Index)
// Considers luminance, contrast, and structure
// More accurate for human perception
return frames.reduce((psi, frame, i) => {
const ssim = calculateSSIM(frame, finalFrame)
const weight = getVisualWeight(frame) // Center-weighted
return psi + (1 - ssim) * weight * interval
}, 0)
}
Best Practices for Optimizing Speed Index
1. Implement Progressive Rendering
Critical CSS Inlining
<!DOCTYPE html>
<html>
<head>
<!-- Inline critical CSS for immediate render -->
<style>
/* Critical above-the-fold styles */
body {
margin: 0;
font-family: system-ui;
}
.header {
height: 60px;
background: #333;
color: white;
}
.hero {
min-height: 400px;
background: linear-gradient(#f0f0f0, #e0e0e0);
}
.content {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Skeleton screen styles */
.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;
}
}
</style>
<!-- Preload key resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
<link rel="preload" href="/css/main.css" as="style" />
<link rel="preload" href="/img/hero.webp" as="image" />
</head>
<body>
<!-- Immediate skeleton -->
<header class="header skeleton"></header>
<div class="hero skeleton"></div>
<main class="content">
<div class="skeleton" style="height: 20px; margin-bottom: 10px;"></div>
<div class="skeleton" style="height: 20px; width: 80%;"></div>
</main>
<!-- Load full styles asynchronously -->
<link
rel="stylesheet"
href="/css/main.css"
media="print"
onload="this.media='all'"
/>
</body>
</html>
Progressive Image Loading
// Low-quality image preview (LQIP) technique
class ProgressiveImage {
constructor(element) {
this.element = element;
this.preview = element.dataset.preview;
this.src = element.dataset.src;
this.loadImage();
}
loadImage() {
// Step 1: Show tiny preview (inline base64)
this.element.src = this.preview;
this.element.classList.add('loading');
// Step 2: Load full image
const img = new Image();
img.src = this.src;
img.onload = () => {
// Step 3: Fade in full image
this.element.src = this.src;
this.element.classList.remove('loading');
};
}
}
// CSS for smooth transition
.progressive-image {
filter: blur(5px);
transition: filter 0.3s;
}
.progressive-image.loading {
filter: blur(20px);
}
2. Optimize Resource Loading Order
Resource Hints
<!-- DNS prefetch for external domains -->
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<!-- Preconnect for critical third-party origins -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<!-- Preload critical resources -->
<link rel="preload" as="style" href="/css/critical.css" />
<link rel="preload" as="font" href="/fonts/main.woff2" crossorigin />
<link rel="preload" as="image" href="/img/hero.webp" />
<link rel="preload" as="script" href="/js/critical.js" />
<!-- Prefetch next page resources -->
<link rel="prefetch" href="/next-page.html" />
<link rel="prefetch" href="/css/next-page.css" />
Priority Hints
<!-- Fetch priority for key images -->
<img src="hero.jpg" fetchpriority="high" alt="Hero" />
<img src="below-fold.jpg" fetchpriority="low" loading="lazy" alt="Secondary" />
<!-- Script priorities -->
<script src="critical.js" fetchpriority="high"></script>
<script src="analytics.js" fetchpriority="low" async></script>
3. Implement Smart Loading Strategies
Adaptive Loading Based on Connection
// Adapt content based on network speed
class AdaptiveLoader {
constructor() {
this.connection =
navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection
this.effectiveType = this.connection?.effectiveType || "4g"
}
loadImages() {
const images = document.querySelectorAll("[data-adaptive]")
images.forEach(img => {
switch (this.effectiveType) {
case "slow-2g":
case "2g":
// Load only preview
img.src = img.dataset.preview
break
case "3g":
// Load medium quality
img.src = img.dataset.medium || img.dataset.src
break
case "4g":
default:
// Load full quality
img.src = img.dataset.src
}
})
}
loadVideos() {
if (this.effectiveType === "slow-2g" || this.effectiveType === "2g") {
// Don't autoload videos on slow connections
document.querySelectorAll("video[autoplay]").forEach(video => {
video.removeAttribute("autoplay")
video.setAttribute("poster", video.dataset.poster)
})
}
}
}
4. Use Service Workers for Instant Loading
App Shell Pattern
// sw.js - Cache app shell for instant Speed Index
const CACHE_NAME = "app-shell-v1"
const shellFiles = ["/", "/css/shell.css", "/js/shell.js", "/img/logo.svg"]
self.addEventListener("install", e => {
e.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(shellFiles)))
})
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(response => {
// Return cached shell instantly (Speed Index ~0ms for shell)
if (response) return response
// Fetch dynamic content
return fetch(e.request).then(response => {
// Cache successful responses
if (!response || response.status !== 200) return response
const responseClone = response.clone()
caches.open(CACHE_NAME).then(cache => {
cache.put(e.request, responseClone)
})
return response
})
})
)
})
5. Optimize Font Loading
Font Loading Strategies
/* Strategy 1: Optional font loading for best Speed Index */
@font-face {
font-family: "Brand";
src: url("/fonts/brand.woff2") format("woff2");
font-display: optional; /* Use only if cached */
}
/* Strategy 2: Swap for important text */
@font-face {
font-family: "Body";
src: url("/fonts/body.woff2") format("woff2");
font-display: swap; /* Show fallback immediately */
}
/* Strategy 3: Preload + swap for critical fonts */
/* In HTML: <link rel="preload" href="/fonts/heading.woff2" as="font" crossorigin> */
@font-face {
font-family: "Heading";
src: url("/fonts/heading.woff2") format("woff2");
font-display: swap;
}
Font Loading API
// Progressive font loading
if ("fonts" in document) {
// Load fonts programmatically
const fontFace = new FontFace("Brand", "url(/fonts/brand.woff2)")
fontFace.load().then(font => {
document.fonts.add(font)
document.body.classList.add("fonts-loaded")
})
// Or wait for all fonts
document.fonts.ready.then(() => {
document.body.classList.add("all-fonts-loaded")
})
}
Common Speed Index Issues
Issue 1: Late-Loading Hero Images
Problem: Large hero images delaying visual progress
Solution: Progressive loading with previews
<!-- Inline SVG preview -->
<div
class="hero"
style="background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjYwMCI+CiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNjAwIiBmaWxsPSIjZTBl')"
>
>
<img
class="hero-image"
src="hero-lowres.jpg"
data-src="hero-fullres.jpg"
loading="eager"
alt="Hero"
/>
</div>
<script>
// Load high-res when ready
const hero = document.querySelector(".hero-image")
const highRes = new Image()
highRes.src = hero.dataset.src
highRes.onload = () => {
hero.src = highRes.src
hero.classList.add("loaded")
}
</script>
Issue 2: Flash of Unstyled Content (FOUC)
Problem: Content appears unstyled, then jumps when CSS loads
Solution: Critical CSS + proper loading order
// Detect and prevent FOUC
document.documentElement.className = "no-fouc"
// Critical CSS prevents FOUC
const criticalCSS = `
.no-fouc { visibility: hidden; opacity: 0; }
.fonts-loaded { visibility: visible; opacity: 1; transition: opacity 0.3s; }
`
// Inject critical CSS
const style = document.createElement("style")
style.textContent = criticalCSS
document.head.appendChild(style)
// Show content when ready
window.addEventListener("load", () => {
document.documentElement.classList.add("fonts-loaded")
})
Issue 3: Slow Third-Party Content
Problem: Ads and widgets delaying visual completion
Solution: Load third-party content after visual completion
// Delay third-party content until after Speed Index window
const loadThirdParty = () => {
// Load only after main content is visible
if (document.readyState === "complete") {
loadAds()
loadSocialWidgets()
loadAnalytics()
}
}
// Wait for visual completion
if ("requestIdleCallback" in window) {
requestIdleCallback(loadThirdParty, { timeout: 3000 })
} else {
setTimeout(loadThirdParty, 3000)
}
Tools for Measuring Speed Index
WebPageTest
WebPageTest provides the most detailed Speed Index analysis:
- Frame-by-frame visual progress
- Filmstrip view
- Speed Index calculation
- Perceptual Speed Index
Lighthouse
# Measure Speed Index with Lighthouse
lighthouse https://example.com --only-audits=speed-index
# Detailed performance metrics
lighthouse https://example.com \
--only-categories=performance \
--output=json \
--output-path=./report.json
# Extract Speed Index
cat report.json | jq '.audits["speed-index"].numericValue'
Custom Speed Index Calculation
// Simplified Speed Index calculation for monitoring
class SpeedIndexMonitor {
constructor() {
this.frames = []
this.startTime = performance.now()
}
captureFrame() {
// Capture visual state (simplified)
const visibleElements = document.querySelectorAll(
"*:not(script):not(style)"
)
const frame = {
time: performance.now() - this.startTime,
elements: visibleElements.length,
images: document.querySelectorAll('img[src]:not([src=""])').length,
text: document.body.innerText.length
}
this.frames.push(frame)
}
calculate() {
if (this.frames.length < 2) return 0
const lastFrame = this.frames[this.frames.length - 1]
let speedIndex = 0
for (let i = 1; i < this.frames.length; i++) {
const frame = this.frames[i]
const prevFrame = this.frames[i - 1]
// Calculate completeness (simplified)
const completeness =
(frame.elements / lastFrame.elements) * 0.3 +
(frame.images / Math.max(1, lastFrame.images)) * 0.4 +
(frame.text / Math.max(1, lastFrame.text)) * 0.3
const interval = frame.time - prevFrame.time
speedIndex += (1 - completeness) * interval
}
return speedIndex
}
}
Frequently Asked Questions
How is Speed Index different from other metrics?
Speed Index measures the entire visual loading progress, not just specific moments. While FCP marks first paint and LCP marks largest paint, Speed Index captures everything in between, providing a holistic view of visual performance.
Why is my Speed Index different between tools?
Different tools may:
- Use different viewport sizes
- Apply different visual comparison algorithms
- Include/exclude certain page elements
- Use different sampling rates (fps)
WebPageTest is generally considered the most accurate.
Can animations affect Speed Index?
Yes, continuous animations can negatively impact Speed Index by preventing visual stability. Consider:
- Pausing animations during initial load
- Using CSS
prefers-reduced-motion - Avoiding auto-playing videos above the fold
How does lazy loading affect Speed Index?
Lazy loading below-the-fold content improves Speed Index by:
- Reducing initial network congestion
- Allowing above-fold content to load faster
- Preventing resource competition
Never lazy-load above-the-fold content as it worsens Speed Index.
What's a realistic Speed Index target?
Targets vary by site type:
- Content sites: < 3000ms
- E-commerce: < 4000ms
- Web apps: < 4500ms
- Mobile: Add 1000ms to desktop targets
Focus on progressive improvement rather than absolute numbers.