Performance & Perceived Speed

Performance is a UX issue, not just an engineering issue. A 1-second delay in page load time reduces conversions by 7% (Akamai). A page that takes over 3 seconds to load loses 53% of mobile visitors (Google). Speed is the most important UX feature you'll never see in a design mockup.

Core Web Vitals

Google's Core Web Vitals are the three metrics that matter most for user experience. They affect search rankings and directly correlate with user satisfaction and conversion.

MetricTargetMeasuresUser Experience Impact
LCP (Largest Contentful Paint)< 2.5sLoading performance: when does the main content appear?"Is this page loading?"
INP (Interaction to Next Paint)< 200msResponsiveness: how fast does the page react to input?"Did my click do anything?"
CLS (Cumulative Layout Shift)< 0.1Visual stability: does content jump around?"I was about to click that and it moved!"

Measuring Core Web Vitals

ToolTypeWhen to Use
Chrome DevTools → PerformanceLab dataDuring development
LighthouseLab dataAutomated audits, CI/CD integration
PageSpeed InsightsLab + Field dataQuick check with real-user data from CrUX
Web Vitals Chrome ExtensionLab dataReal-time monitoring during development
Google Search ConsoleField dataMonitoring across entire site over time
Chrome UX Report (CrUX)Field dataReal user data at scale
web-vitals JS libraryField dataCustom analytics integration
// Measure Core Web Vitals with the web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(metric => sendToAnalytics('LCP', metric.value));
onINP(metric => sendToAnalytics('INP', metric.value));
onCLS(metric => sendToAnalytics('CLS', metric.value));

LCP: Largest Contentful Paint

The time it takes for the largest visible content element (image, heading, text block) to render.

Common causes of poor LCP:

CauseFix
Slow server responseUse a CDN, optimize server-side rendering, upgrade hosting
Render-blocking CSS/JSInline critical CSS, defer non-critical JS
Large unoptimized imagesCompress images, use modern formats (WebP/AVIF), proper sizing
Web fonts blocking renderUse font-display: swap, preload key fonts
Client-side rendering delayServer-side render the critical content
<!-- Preload the LCP image -->
<link rel="preload" as="image" href="/hero-image.webp">

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

INP: Interaction to Next Paint

Measures the delay between a user interaction (click, tap, key press) and the next visual update. Replaced FID (First Input Delay) in 2024.

Common causes of poor INP:

CauseFix
Long JavaScript tasks blocking main threadBreak tasks into smaller chunks using requestIdleCallback or setTimeout
Expensive re-rendersOptimize React/Vue rendering, use memo, virtualize long lists
Heavy event handlersDebounce/throttle scroll and resize handlers
Large DOMReduce DOM nodes, use virtualization for large lists
Third-party scriptsAudit and remove unnecessary scripts, load async

CLS: Cumulative Layout Shift

Measures how much page content unexpectedly shifts during the page lifecycle.

Common causes of CLS:

CauseFix
Images without dimensionsAlways set width and height attributes (or aspect-ratio in CSS)
Ads or embeds loading lateReserve space with fixed dimensions
Web fonts causing text reflowUse font-display: swap + size-adjust for close metrics
Dynamic content inserted above viewportInsert below the fold, or use a placeholder
Late-loading CSSInline critical CSS, preload stylesheets
<!-- Always specify image dimensions to prevent CLS -->
<img src="photo.webp" width="800" height="600" alt="Description"
  style="max-width: 100%; height: auto;">
/* Reserve space with aspect-ratio */
.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #f0f0f0;
}

Response Time Psychology

How users perceive different wait times:

DurationUser PerceptionRequired FeedbackExample
< 100msInstantNoneTyping, button hover
100-300msSlight delayButton state changeClick response, toggle
300ms-1sNoticeableSubtle indicatorContent loading, spinner
1-3sUser is waitingProgress indicator or skeletonPage load, API response
3-5sAttention wandersProgress bar + estimateFile upload, search
5-10sFrustrationProgress bar + time estimate + allow cancelLarge file upload
> 10sTask abandonedBackground processing + notificationVideo processing, export

The Doherty Threshold

When system response is under 400ms, users and computers establish a conversational rhythm. Productivity increases dramatically. Above 400ms, users lose their flow state. Target < 400ms for all common interactions.

Perceived Performance Techniques

You can't always make things actually faster. But you can make them feel faster.

1. Optimistic UI

Show the result immediately, sync with the server in the background.

User clicks "Like":
┌────────────────────────┐
│ ♡ 42 likes             │  User clicks heart
└────────────────────────┘
         ↓ (instant)
┌────────────────────────┐
│ ♥ 43 likes             │  Immediately updated (no spinner)
└────────────────────────┘
         ↓ (background)
Server saves. If it fails → revert to ♡ 42 + show error toast.

Use for: Likes, favorites, toggles, adding to lists, marking as read. Don't use for: Payments, deleting data, sending messages.

2. Skeleton Screens

Show the structure of the content while it loads. Better than spinners because they set expectations about the layout.

Loading...                    vs.    Skeleton:
┌────────────────────────┐          ┌────────────────────────┐
│                        │          │ ░░░░░░░░░░░░░░░░░░░░   │
│                        │          │                        │
│       ⟳ Loading...     │          │ ░░░░░░░░░░░░░░░░░░░░░  │
│                        │          │ ░░░░░░░░░░░░░░░░░░░░░  │
│                        │          │ ░░░░░░░░░░░░           │
│                        │          │                        │
│                        │          │ ░░░░░░░░               │
└────────────────────────┘          └────────────────────────┘
"I have no idea what's coming"      "I can see what's about to appear"

Rules for skeletons:

  • Match the actual content shape (text lines, image placeholders, card outlines)
  • Use a shimmer/wave animation (not pulsing; it's distracting)
  • Don't show skeletons for content that loads in < 300ms (unnecessary visual noise)
  • Replace skeleton with content seamlessly (no flash)

3. Progressive Loading

Load and display content in priority order:

1. First: Text content (fastest, most important)
2. Second: Above-fold images
3. Third: Below-fold images (lazy loaded)
4. Fourth: Interactive features, animations
5. Last: Analytics, non-essential third-party scripts

4. Preloading and Prefetching

Anticipate what the user will do next and load it before they click.

<!-- Preload: high priority, needed for current page -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero.webp" as="image">

<!-- Prefetch: lower priority, needed for likely next page -->
<link rel="prefetch" href="/dashboard">

<!-- Preconnect: establish connection to third-party origin -->
<link rel="preconnect" href="https://api.example.com">

<!-- DNS Prefetch: resolve DNS for third-party origin -->
<link rel="dns-prefetch" href="https://analytics.example.com">

Smart prefetching:

  • Prefetch the most likely next page (e.g., after login → prefetch dashboard)
  • Prefetch on hover for navigation links (200ms delay gives ~300ms head start)
  • Don't prefetch everything. It wastes bandwidth on mobile

5. Instant Navigation Feedback

Even if the page takes time to load, acknowledge the click immediately:

PatternImplementation
Navigation linkHighlight immediately on click, show loading bar at top of page
Button submissionDisable button + show spinner within 50ms of click
Tab switchSwitch tab indicator immediately, load content in the tab area
SearchShow results container immediately, populate as results arrive

6. Progress Indicators

For operations > 3 seconds, show progress:

TypeWhen to UseImplementation
Determinate (percentage)When you know the total workFile upload: "45%, 2.3 of 5.1 MB"
Indeterminate (spinner)When you don't know how long it'll takeAPI calls, search queries
SteppedMulti-phase operations"Step 2 of 4: Processing images..."
Fake progressWhen there's no real progress infoStart at 0%, quickly jump to 60%, slowly to 90%, hold, jump to 100% on completion

Image Optimization

Images are typically the heaviest assets on a page and the biggest opportunity for performance gains.

Format Selection

FormatBest ForBrowser SupportSize vs. JPEG
JPEGPhotos, complex imagesUniversalBaseline
PNGGraphics with transparencyUniversalLarger than JPEG for photos
WebPPhotos and graphics97%+25-35% smaller than JPEG
AVIFPhotos and graphics92%+50% smaller than JPEG
SVGIcons, logos, illustrationsUniversalTiny for simple graphics
<!-- Use picture element for format fallback -->
<picture>
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="Description" width="800" height="600"
    loading="lazy" decoding="async">
</picture>

Responsive Images

Serve different sizes based on viewport:

<img
  srcset="photo-400w.webp 400w,
          photo-800w.webp 800w,
          photo-1200w.webp 1200w,
          photo-1600w.webp 1600w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1000px) 50vw,
         33vw"
  src="photo-800w.webp"
  alt="Description"
  width="800" height="600"
  loading="lazy"
  decoding="async"
>

Image Loading Strategy

PositionLoading Strategy
Above the fold (hero, LCP)Eager load, preload, no lazy loading
Just below the foldLazy load with loading="lazy"
Far below the foldLazy load, consider lower quality
ThumbnailsServe tiny sizes (200-400px wide)
Background/decorativeLazy load, consider CSS-only alternatives

JavaScript and Bundle Optimization

TechniqueDescriptionImpact
Code splittingSplit bundle by route/feature, load only what's neededMajor reduction in initial load
Tree shakingRemove unused code during build10-50% bundle reduction
Dynamic importsLoad features only when neededFaster initial load
Lazy loading routesOnly load route code when navigated toFaster initial load
Dependency auditReplace heavy libraries with lighter alternativesCan save 100KB+
CompressionUse Brotli or gzip for text assets60-80% transfer size reduction
// Dynamic import — only load chart library when user visits analytics page
const loadChartLibrary = () => import('./charts');

// Route-level code splitting in React
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));

Font Optimization

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;       /* Show fallback immediately */
  unicode-range: U+0000-00FF; /* Only load Latin characters */
}
StrategyImpact
Use variable fonts (single file for all weights)1 request instead of 3-5
Subset to needed character ranges50-80% smaller font files
Self-host instead of Google FontsEliminate third-party request
Use font-display: swapNo invisible text during load
Preload the primary fontFont available sooner
Use system font stack as fallbackInstant text rendering

Performance Budget

Set explicit limits and fail builds that exceed them:

ResourceBudget
Total page weight< 1.5 MB (mobile), < 3 MB (desktop)
JavaScript< 300 KB (compressed)
CSS< 100 KB (compressed)
Images (above fold)< 200 KB total
Web fonts< 100 KB total
LCP< 2.5 seconds
INP< 200 ms
CLS< 0.1
Time to Interactive< 3.8 seconds

Common Mistakes

MistakeImpactFix
No image dimensions specifiedCLS as images load and push contentAlways set width/height on img tags
Loading all JS upfrontMassive initial bundle, slow loadCode split by route, lazy load features
Serving desktop images on mobile3× more data than neededUse srcset with responsive image sizes
Too many third-party scriptsEach script adds DNS lookup + download + executionAudit scripts quarterly, remove unnecessary ones
No caching strategyEvery visit downloads everything againSet appropriate Cache-Control headers
Using GIFs for animationsGIFs are enormousUse MP4/WebM video or CSS animations
Spinner for everythingDoesn't set expectations, feels slowUse skeleton screens for content loading
Not measuring real user performanceLab data ≠ real experienceUse web-vitals library + real user monitoring

Key Takeaways

  • Core Web Vitals (LCP < 2.5s, INP < 200ms, CLS < 0.1) are the minimum targets. Measure with real user data, not just lab data.
  • Perceived speed matters as much as actual speed. Use skeleton screens, optimistic UI, and progressive loading.
  • Respond to every user action within 100ms. If the backend takes longer, show feedback immediately and sync in the background.
  • Images are the biggest opportunity: use modern formats (WebP/AVIF), responsive sizes, and lazy loading.
  • Set a performance budget and enforce it in CI/CD. Performance degrades unless you actively protect it.
  • Preload critical resources (LCP image, key font), prefetch likely next pages.
  • Every millisecond matters: 100ms delay = 7% lower conversion. Performance is a business metric, not just a technical one.