Last updated: Apr 29, 2026
Loading Optimization
Page load speed depends on how efficiently assets travel from server to browser. The optimizations below target different parts of that journey — reducing bytes on the wire, avoiding unnecessary requests, and loading resources in the right order.
Compression
Servers can compress responses before sending them. The browser advertises supported encodings via the Accept-Encoding header, and the server picks one.
| Encoding | Notes |
|---|---|
| gzip | Universally supported, good baseline compression |
Brotli (br) | 15–25 % smaller than gzip for text assets; supported by all modern browsers over HTTPS |
Enable Brotli with a gzip fallback. Most CDNs and reverse proxies (Nginx, Cloudflare) handle this automatically.
Caching
Proper cache headers eliminate repeat downloads entirely.
| Header | Role |
|---|---|
Cache-Control: max-age=31536000, immutable | Long-lived cache for fingerprinted assets (hashed filenames) |
Cache-Control: no-cache | Always revalidate with the server before using the cached copy |
ETag / Last-Modified | Let the server respond with 304 Not Modified when the resource has not changed |
Expires | Legacy equivalent of max-age; Cache-Control takes precedence when both are present |
Fingerprint static assets (e.g. app.3f2a1b.js) and serve them with a long max-age. HTML documents themselves should use no-cache so users always get the latest entry point.
HTTP/2
HTTP/2 multiplexes many requests over a single TCP connection, removing the head-of-line blocking that limited HTTP/1.1 to ~6 parallel requests per origin. This makes several HTTP/1.1-era hacks unnecessary:
- File concatenation — bundling all CSS or JS into a single file is no longer required for performance; smaller, granular files work well with multiplexing.
- Image sprites — individual images can be served without the overhead of extra connections.
- Domain sharding — splitting assets across subdomains to increase parallelism is counterproductive under HTTP/2.
If the server still runs HTTP/1.1, concatenating CSS and JS into single files and using image sprites remain valid optimizations.
Resource Hints
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="prefetch" href="/next-page.js" />
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
<link rel="modulepreload" href="/src/app.js" />
<link rel="prerender" href="https://my-app.com/pricing" />
| Hint | When to use |
|---|---|
preload | Resource is needed for the current page but discovered late (fonts, critical scripts behind an @import) |
prefetch | Resource is likely needed for the next navigation; fetched at idle priority |
preconnect | You know a request to an origin is coming soon; performs DNS lookup, TCP handshake, and TLS negotiation early |
dns-prefetch | Lighter alternative to preconnect — resolves DNS only; useful for third-party origins that may be needed |
modulepreload | Like preload but for ES modules — also parses and compiles the module so it is ready to execute immediately |
prerender | Loads and renders an entire page in a hidden tab; use only when very confident the user will navigate there next |
preconnect is most valuable for origins used above the fold (API host, font CDN). Limit it to 4–6 origins — each open connection has a cost. Fall back to dns-prefetch for lower-priority or less-certain origins, since it carries almost no overhead. prerender is the heaviest hint — it downloads the full page and its subresources. Use it for at most one URL and only when navigation is highly likely (e.g. a dominant next step in a funnel).
Code Delivery
Minification and tree-shaking are covered in Build Pipeline. Two additional techniques reduce the JavaScript a page actually downloads:
Code splitting breaks a bundle into smaller chunks loaded on demand. Route-based splitting is the most common approach — each page only downloads its own code.
const Settings = lazy(() => import("./pages/Settings"));
Critical CSS inlines the styles needed for above-the-fold content directly in the <head>, then loads the full stylesheet asynchronously. This avoids a render-blocking request for the entire CSS file.
<head>
<style>
/* inlined critical styles */
</style>
<link
rel="preload"
href="/styles.css"
as="style"
onload="this.rel='stylesheet'"
/>
</head>
Images
Images are typically the heaviest assets on a page. That said, raw byte size can be misleading — 300 KB of JavaScript costs far more than 300 KB of an image because JS must be parsed, compiled, and executed, while an image only needs to be decoded and painted.
| Technique | Details |
|---|---|
| Modern formats | WebP and AVIF offer significantly better compression than JPEG/PNG at comparable quality |
| Compression | Run images through tools like sharp, squoosh, or imagemin before deployment |
| Responsive images | Use srcset and sizes to serve appropriately sized images per viewport |
| Sprites | Combine small icons into a single image to reduce requests (HTTP/1.1 only — unnecessary with HTTP/2) |
Lazy loading defers off-screen images so they do not block the initial page load.
<img src="photo.webp" loading="lazy" alt="description" />
The native loading="lazy" attribute works in all modern browsers. For more control (e.g. fade-in effects, custom thresholds), the Intersection Observer API can trigger loading manually:
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
observer.unobserve(img);
}
}
});
document
.querySelectorAll("img[data-src]")
.forEach((img) => observer.observe(img));
Fonts
| Practice | Reason |
|---|---|
| Serve WOFF2 with WOFF fallback | WOFF2 compresses ~30 % better than WOFF; WOFF covers legacy browsers |
font-display: swap | Shows a system font immediately, swaps to the custom font once loaded — avoids invisible text |
| Preload critical font files | Fonts are discovered late (after CSS is parsed); preloading starts the download earlier |
@font-face {
font-family: "Inter";
src:
url("/fonts/inter.woff2") format("woff2"),
url("/fonts/inter.woff") format("woff");
font-display: swap;
}
Script Loading
Where and how a <script> tag appears determines when it runs relative to HTML parsing.
| Attribute | Behavior |
|---|---|
| (none) | Blocks parsing while the script downloads and executes |
async | Downloads in parallel with parsing; executes immediately when ready (order not guaranteed) |
defer | Downloads in parallel with parsing; executes after the document is parsed, before DOMContentLoaded (order preserved) |
End of <body> | Equivalent to defer in practice — parsing finishes before the script is encountered |
Use defer for scripts that depend on the DOM or on each other. Use async for independent scripts like analytics.
Perceived Performance
Skeleton screens display placeholder shapes that mirror the layout of incoming content. Unlike a blank page or a spinner, they give users an immediate sense of structure. The skeleton is replaced with real content as data arrives — this reduces perceived wait time even when actual load time is the same.