Performance Fundamentals
Performance means efficiency. A fast app delivers its content quickly and responds to user interactions without delay. Poor performance directly costs you users — research consistently shows that slower pages lose visitors, conversions, and engagement.
The Three Pillars
How Browsers Work
Understanding how browsers process a URL into visible pixels is the foundation of all performance work. Every optimization maps back to shortening or parallelizing this pipeline.
The Full Page Load Pipeline
<script> tag in <head> halts everything until the JS downloads, parses, and executes. Use defer or async.
The browser uses a preload scanner — a secondary parser that looks ahead in the HTML stream to fetch resources like CSS, fonts, and images before the main parser reaches them.
Critical Rendering Path
The Critical Rendering Path (CRP) is the sequence of steps to convert HTML + CSS + JS into pixels. Optimizing it means reducing the number of blocking steps and getting the first paint to happen as fast as possible.
The Four Key Objects
display: none elements are excluded.Render-Blocking Resources
By default, CSS is render-blocking and JS is both parser-blocking and render-blocking. Minimize both.
<!-- Blocks parsing entirely (avoid) -->
<script src="app.js"></script>
<!-- Downloads in parallel, executes after HTML parsed -->
<script src="app.js" defer></script>
<!-- Downloads in parallel, executes immediately when ready -->
<script src="analytics.js" async></script>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="large.css" media="(min-width:1024px)">
Understanding Latency
Latency is the time for a data packet to travel from source to destination. It's measured in milliseconds and is determined by physical distance and network hops — you cannot compress it to zero.
What Affects Latency
- Geographic distance — packets travel at ~2/3 the speed of light through fiber
- Number of network hops — each router adds ~1–5ms
- Protocol overhead — TCP handshakes, TLS negotiations add round trips
- Network congestion — shared infrastructure causes variable latency
- Last-mile connection — WiFi, 4G, and cable all have different base latencies
Reducing Latency
- Use a CDN — serve assets from edge nodes close to users
- Enable HTTP/2 or HTTP/3 — multiplexing eliminates head-of-line blocking
- Use Connection: keep-alive — reuse TCP connections
- Implement service workers — serve cached responses with ~0ms latency
- Enable QUIC/HTTP3 — reduces handshake round trips significantly
Timing Guidelines: How Long is Too Long?
Human perception defines performance budgets. These are the thresholds research has established for different interaction types:
Lazy Loading
Lazy loading defers loading of non-critical resources until they're actually needed — typically when they scroll into view. It shortens the critical rendering path by reducing initial page weight.
Native HTML Lazy Loading
<!-- Browser-native, no JS needed -->
<img src="hero.jpg" loading="eager" alt="..."> <!-- above fold -->
<img src="photo.jpg" loading="lazy" alt="..."> <!-- below fold -->
<iframe src="map.html" loading="lazy"></iframe>
Intersection Observer (JS)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('[data-src]').forEach(img => {
observer.observe(img);
});
Speculative Loading
While lazy loading defers work, speculative loading does the opposite — it preemptively fetches resources the user is likely to need next. When a guess is right, the navigation feels instantaneous.
Resource Hints Hierarchy
<!-- Resolve DNS only (very cheap, use liberally) -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<!-- DNS + TCP + TLS (moderate cost) -->
<link rel="preconnect" href="https://api.example.com">
<!-- Fetch + cache the resource (use for certain resources) -->
<link rel="prefetch" href="/next-page.html">
<!-- Fetch + execute immediately, highest priority -->
<link rel="preload" as="font" href="font.woff2" crossorigin>
<link rel="preload" as="image" href="hero.avif">
<!-- Speculatively render entire next page (Chrome 108+) -->
<script type="speculationrules">
{ "prerender": [{ "source": "list", "urls": ["/shop"] }] }
</script>
DNS Prefetch
Every unique hostname requires a DNS lookup before the browser can connect. On slow networks, this can take 20–120ms. DNS prefetch resolves these in the background, eliminating that delay when the resource is actually requested.
<!-- Prefetch DNS for third-party origins -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.provider.io">
<!-- For origins you'll connect to soon, use preconnect instead -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
preconnect for your top 1–2 most critical third-party origins (e.g. font CDN). Use dns-prefetch as a fallback for browsers that don't support preconnect, and for all other third-party origins you'll eventually need.
Optimizing Startup Performance
Startup is your first impression. A long startup makes users think the app crashed. The goal: get something visible and interactive on screen as fast as possible.
Key Strategies
- Code splitting — split your JS bundle; load only what's needed for the initial view
- Tree shaking — eliminate unused code from bundles at build time
- Inline critical CSS — put above-the-fold styles in a
<style>tag in<head> - Preload the LCP image — use
<link rel="preload">for your hero image - Minimize render-blocking JS — defer everything not needed for first render
- Server-side rendering (SSR) — send pre-rendered HTML so browser can paint immediately
- Reduce server response time — target under 200ms TTFB (Time to First Byte)
Animation Performance & Frame Rate
Smooth animation requires 60 frames per second — meaning each frame has only 16.7ms for the browser to calculate layout, paint, and composite. Miss the budget and you get jank.
CSS vs JS Animations
CSS Animations ✓ Prefer These
- Run on the compositor thread (off main thread)
transformandopacityare GPU-accelerated- No JavaScript overhead
- Browser can optimize and skip frames gracefully
JS Animations — Use Carefully
- Run on the main thread by default
- Can be blocked by other JS execution
- Use
requestAnimationFrame— neversetInterval - Web Animations API gives JS with compositor benefits
The Compositing Secret
Animating transform and opacity only triggers the composite step — no layout, no paint. This is why they're the only "free" properties to animate.
/* ✅ Compositor only — free to animate */
transform: translateX(100px);
opacity: 0.5;
/* ⚠️ Triggers paint — moderate cost */
color, background-color, box-shadow, border-radius;
/* ❌ Triggers layout (reflow) — expensive */
width, height, top, left, margin, padding, font-size;
will-change: transform to promote an element to its own compositor layer before animation begins. This eliminates promotion cost at animation start. Use sparingly — each layer uses GPU memory.
RUM vs. Synthetic Monitoring
Two complementary approaches to measuring performance in production. Use both for a complete picture.
Real User Monitoring (RUM)
- Measures actual users on real devices/networks
- Captures the long tail (slow devices, rural users)
- Best for spotting long-term regressions
- Requires traffic — no data for new pages
- Examples: SpeedCurve, Datadog RUM, web-vitals.js
Synthetic Monitoring
- Controlled, scripted tests from fixed locations
- Works before launch — no users needed
- Best for catching regressions in CI/CD
- Consistent baseline for comparisons
- Examples: WebPageTest, Lighthouse CI, Calibre
Performance Budgets
A performance budget is a hard limit that prevents regressions from shipping. It makes performance a first-class engineering constraint, not an afterthought.
Types of Budgets
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000, // 250kb per asset
maxEntrypointSize: 400000, // 400kb entrypoint
hints: 'error', // fail the build, not just warn
}
};
Performance Checklist
Your launch checklist for web performance fundamentals:
- Use
deferorasyncon all non-critical scripts - Inline critical CSS; load the rest asynchronously
- Add
loading="lazy"to all below-fold images and iframes - Preload the LCP image with
<link rel="preload" as="image"> - Use
dns-prefetch/preconnectfor third-party origins - Enable HTTP/2 or HTTP/3 on your server
- Compress all text assets with Brotli or gzip
- Serve images in WebP or AVIF format
- Set aggressive cache headers for static assets
- Only animate
transformandopacity - Use
requestAnimationFramefor JS-driven animations - Code-split your JS bundle; load only what's needed
- Measure with Lighthouse CI in your CI/CD pipeline
- Set and enforce performance budgets per page type
- Monitor real users with RUM tooling in production