http/1.1, investigate reverse proxy configurations, CDN origin settings, or legacy server blocks that may be downgrading the connection.
Step 2: Consolidate Origins and Remove Domain Sharding
HTTP/2 multiplexing operates most efficiently over a single TCP connection. Domain sharding forces the browser to establish multiple TLS handshakes, negotiate separate HTTP/2 settings, and manage independent flow control windows. Consolidate all static assets to a single origin. Modern CDNs handle high concurrency natively, making subdomain fragmentation counterproductive.
Step 3: Replace Monolithic Bundles with Code-Split Modules
Concatenation was designed to reduce HTTP requests. Under HTTP/2, 30 individual modules load with nearly identical latency to one large bundle, but with dramatically better cache behavior. Configure the bundler to split vendor dependencies, runtime utilities, and route-specific code into separate chunks. Use long-lived caching headers for vendor modules that change infrequently, and content-hashed filenames for application code that updates regularly.
Step 4: Decouple UI Assets from CSS
CSS sprites and base64 inlining were workarounds for request overhead. HTTP/2 handles parallel image requests efficiently. Replace sprite sheets with individual SVG components or icon libraries loaded on demand. Remove base64-encoded images from stylesheets; instead, reference external assets and use browser priority hints to control loading order.
Step 5: Implement Priority Signaling
HTTP/2 allows servers and clients to signal resource importance. Use <link rel="preload"> for critical CSS and fonts, and fetchpriority="high" for above-the-fold images. Configure the server or CDN to respect these hints and schedule delivery accordingly. This replaces the old practice of inlining critical assets directly into HTML or CSS.
Architecture Decisions and Rationale
Why single origin over sharding?
HTTP/2 multiplexing eliminates the six-connection ceiling. A single connection reduces TLS negotiation overhead, simplifies connection pooling, and allows the server to prioritize responses across all asset types. Multiple origins fragment the connection pool and force redundant handshakes.
Why fine-grained modules over concatenation?
Cache efficiency directly impacts repeat visit performance. When a single module changes, only that module's hash updates. Users retain cached versions of unchanged vendor libraries and utilities. Concatenation invalidates the entire payload on minor changes, forcing full re-downloads.
Why external assets over base64 inlining?
Base64 encoding increases payload size by approximately 33%. Inlining critical assets blocks parsing of the containing file. External assets load in parallel, can be cached independently, and do not delay critical rendering paths when properly prioritized.
Modern Asset Pipeline Implementation (TypeScript)
The following example demonstrates a modular asset loader that replaces legacy concatenation and inlining patterns. It uses dynamic imports, priority hints, and independent caching strategies.
// asset-pipeline.ts
interface AssetConfig {
criticalCss: string;
vendorModules: string[];
routeChunks: Record<string, string>;
iconAssets: Record<string, string>;
}
class ModernAssetLoader {
private config: AssetConfig;
private cacheRegistry: Map<string, boolean> = new Map();
constructor(config: AssetConfig) {
this.config = config;
}
async initialize(): Promise<void> {
this.injectPriorityHints();
await this.loadVendorDependencies();
}
private injectPriorityHints(): void {
const head = document.head;
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.href = this.config.criticalCss;
preloadLink.as = 'style';
head.appendChild(preloadLink);
const criticalStyle = document.createElement('link');
criticalStyle.rel = 'stylesheet';
criticalStyle.href = this.config.criticalCss;
head.appendChild(criticalStyle);
}
private async loadVendorDependencies(): Promise<void> {
const vendorPromises = this.config.vendorModules.map(async (modulePath) => {
if (this.cacheRegistry.has(modulePath)) return;
const module = await import(/* @vite-ignore */ modulePath);
this.cacheRegistry.set(modulePath, true);
return module;
});
await Promise.all(vendorPromises);
}
async loadRouteChunk(routeName: string): Promise<unknown> {
const chunkPath = this.config.routeChunks[routeName];
if (!chunkPath) throw new Error(`Route chunk not found: ${routeName}`);
const chunk = await import(/* @vite-ignore */ chunkPath);
return chunk.default || chunk;
}
async fetchIcon(iconName: string): Promise<string> {
const svgPath = this.config.iconAssets[iconName];
if (!svgPath) throw new Error(`Icon not registered: ${iconName}`);
const response = await fetch(svgPath, { priority: 'high' });
return await response.text();
}
}
export { ModernAssetLoader, AssetConfig };
This implementation replaces monolithic bundling with route-level code splitting, independent icon fetching, and explicit priority signaling. Each asset type is cached separately, updated independently, and loaded only when required. The architecture aligns with HTTP/2's multiplexing capabilities by treating network requests as cheap operations and cache granularity as the primary optimization target.
Pitfall Guide
1. Monolithic Vendor Bundling
Explanation: Combining all third-party libraries into a single vendor.js file means any minor update to one dependency invalidates the entire bundle. Users must re-download unchanged libraries like React, Lodash, or date utilities on every deployment.
Fix: Split vendor dependencies by update frequency. Use content-hashed filenames for application code and stable filenames for rarely-updated libraries. Configure the bundler to isolate runtime utilities from heavy dependencies.
2. Premature Asset Inlining
Explanation: Embedding images, fonts, or critical CSS directly into HTML or JavaScript increases payload size and blocks parser execution. Base64 encoding adds 33% overhead, and inlined assets cannot be cached independently.
Fix: Keep assets external. Use <link rel="preload"> for critical resources and fetchpriority="high" for above-the-fold content. Let the browser manage parallel loading instead of forcing synchronous parsing.
3. Cross-Origin Sharding
Explanation: Distributing assets across cdn1.example.com, cdn2.example.com, etc., was necessary under HTTP/1.1 to bypass the six-connection limit. Under HTTP/2, each origin requires a separate TCP connection, TLS handshake, and HTTP/2 settings negotiation.
Fix: Consolidate to a single origin. Modern CDNs support high concurrency over multiplexed streams. Remove subdomain routing rules and update asset URLs to point to the primary domain.
4. Ignoring Server-Side Prioritization
Explanation: HTTP/2 allows servers to schedule response delivery based on resource importance. Many teams deploy HTTP/2 but leave priority signaling unconfigured, resulting in images loading before critical CSS or fonts.
Fix: Configure the CDN or origin server to respect Link headers with rel=preload and as attributes. Use HTTP/2 priority frames or modern fetchpriority attributes to guide delivery order.
5. Sprite Sheet Overuse
Explanation: CSS sprites reduce request count but force the browser to download the entire image sheet, even if only one icon is used. This wastes bandwidth and prevents individual icon caching.
Fix: Replace sprites with individual SVG components or icon fonts. Load icons on demand using dynamic imports or fetch calls. Modern HTTP/2 handles dozens of small parallel requests without queuing.
6. Cookie Domain Fragmentation
Explanation: Serving static assets from a cookieless domain was designed to reduce request header overhead. Under HTTP/2, the connection establishment cost of a separate origin outweighs the minor byte savings from cookie exclusion.
Fix: Keep assets on the primary domain. Use modern cookie attributes like SameSite, Partitioned, and CDN-level cookie stripping to minimize header bloat without fragmenting connections.
7. Over-Bundling Development Environments
Explanation: Some teams apply production-level concatenation to development builds to "speed up" local servers. This breaks hot module replacement, increases rebuild times, and masks HTTP/2 behavior differences.
Fix: Use unbundled ES module serving in development. Modern dev servers leverage HTTP/2 multiplexing to serve individual modules efficiently. Reserve concatenation and minification for production builds only.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-traffic SPA with frequent updates | Fine-grained code splitting + long-lived vendor chunks | Maximizes cache hit rates; minimizes re-downloads on minor changes | Lower bandwidth costs; faster repeat visits |
| Marketing site with static assets | Single origin + HTTP/2 multiplexing + preload hints | Simplifies infrastructure; leverages native parallelism | Reduced CDN complexity; lower TLS overhead |
| Legacy monolith migration | Gradual chunk extraction + cookie domain consolidation | Avoids full rewrite; isolates performance gains | Moderate initial refactoring; long-term cache efficiency |
| Image-heavy dashboard | Individual SVG/icon loading + fetchpriority | Prevents unused asset downloads; prioritizes critical UI | Lower initial payload; improved perceived performance |
Configuration Template
The following Vite configuration demonstrates HTTP/2-optimized bundling with code splitting, cache-friendly filenames, and asset decoupling.
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
utilities: ['lodash-es', 'date-fns'],
runtime: ['./src/runtime/init.ts']
},
chunkFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
},
target: 'es2020',
cssCodeSplit: true,
sourcemap: 'hidden'
},
server: {
hmr: {
protocol: 'ws'
}
}
});
Pair this with an HTML template that uses priority signaling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preload" href="/assets/css/critical.css" as="style" />
<link rel="stylesheet" href="/assets/css/critical.css" />
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<div id="root"></div>
<img src="/assets/img/hero.webp" fetchpriority="high" alt="Hero" />
</body>
</html>
Quick Start Guide
- Verify Protocol Support: Open your application in a modern browser, open developer tools, navigate to the Network tab, and enable the Protocol column. Confirm critical assets show
h2 or h3.
- Update Bundler Configuration: Replace monolithic output settings with manual chunk splitting. Separate vendor dependencies, utilities, and runtime code. Enable content hashing for cache invalidation control.
- Remove Legacy Optimizations: Delete domain sharding rules, strip base64-encoded assets from stylesheets, and replace CSS sprites with individual SVG imports. Update asset URLs to point to the primary origin.
- Add Priority Hints: Insert
<link rel="preload"> for critical CSS and fonts. Apply fetchpriority="high" to above-the-fold images. Configure your CDN to respect these signals during delivery.
- Validate Performance: Run a Lighthouse audit and compare repeat visit metrics against baseline measurements. Monitor cache hit rates, Time to Interactive, and total transferred bytes to confirm the migration delivers measurable improvements.