Front-end Engineering Lab

HTTP/2 Optimization

Leverage HTTP/2 features like multiplexing, server push, and header compression

HTTP/2 Optimization

HTTP/2 brings major performance improvements over HTTP/1.1. Understanding and leveraging its features can significantly boost your application's speed.

HTTP/1.1 vs HTTP/2

HTTP/1.1 Problems

1. Head-of-line blocking: Requests queue up
2. Multiple connections: 6-8 connections per domain
3. Large headers: Repeated on every request
4. No prioritization: All requests equal priority
5. Text protocol: Inefficient parsing

HTTP/2 Solutions

1. Multiplexing: Multiple requests over single connection
2. Single connection: One connection per domain
3. Header compression: HPACK algorithm
4. Stream prioritization: Critical resources first
5. Binary protocol: Faster parsing
6. Server push: Proactively send resources

Multiplexing

HTTP/2 allows multiple requests/responses simultaneously over a single connection.

HTTP/1.1 (Serial)

Connection 1: Request CSS → Wait → Response CSS
Connection 2: Request JS  → Wait → Response JS
Connection 3: Request IMG → Wait → Response IMG

Problem: Limited connections, head-of-line blocking

HTTP/2 (Parallel)

Single Connection:
Stream 1: Request CSS ⟷ Response CSS
Stream 2: Request JS  ⟷ Response JS
Stream 3: Request IMG ⟷ Response IMG

All happen simultaneously!

Impact on Bundling

HTTP/1.1: Bundle everything to reduce requests
- bundle.js (500 KB)
- bundle.css (100 KB)

HTTP/2: Split into smaller files
- home.js (50 KB)
- auth.js (30 KB)
- dashboard.js (80 KB)
- shared.js (40 KB)
- ...

Benefits:
- Better caching (change home.js, others cached)
- Parallel downloads
- Faster parsing

Server Push

Server push allows the server to send resources before the client requests them.

Example Flow

Client: Requests index.html

Server: Sends index.html
        + PUSH: style.css
        + PUSH: script.js
        + PUSH: logo.svg

Client: Receives everything immediately

Implementation

Nginx

location / {
  # Push critical resources
  http2_push /css/critical.css;
  http2_push /js/app.js;
  http2_push /fonts/inter-var.woff2;
  
  root /var/www/html;
  index index.html;
}

Node.js (http2 module)

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert'),
});

server.on('stream', (stream, headers) => {
  if (headers[':path'] === '/') {
    // Push critical resources
    stream.pushStream({ ':path': '/css/app.css' }, (err, pushStream) => {
      pushStream.respondWithFile('public/css/app.css', {
        'content-type': 'text/css',
      });
    });

    stream.pushStream({ ':path': '/js/app.js' }, (err, pushStream) => {
      pushStream.respondWithFile('public/js/app.js', {
        'content-type': 'application/javascript',
      });
    });

    // Send HTML
    stream.respondWithFile('public/index.html', {
      'content-type': 'text/html',
    });
  }
});

server.listen(443);
// Express
app.get('/', (req, res) => {
  res.set('Link', [
    '</css/app.css>; rel=preload; as=style',
    '</js/app.js>; rel=preload; as=script',
    '</fonts/inter.woff2>; rel=preload; as=font; crossorigin',
  ].join(', '));
  
  res.sendFile('index.html');
});

What to Push

✅ Push:
- Critical CSS
- Hero image
- Primary JavaScript bundle
- Web fonts

❌ Don't Push:
- Non-critical resources
- Resources likely cached
- Large files (> 50 KB)
- User-specific data

Push Caching Problem

Problem: Server can't know if client has cached resource

Solution: Cache-Digest header (experimental)
Client sends digest of cached resources
Server only pushes what's missing

Header Compression (HPACK)

HTTP/2 compresses headers, reducing overhead significantly.

HTTP/1.1 (Repeated Headers)

Request 1:
  GET /api/users HTTP/1.1
  Host: api.example.com
  User-Agent: Mozilla/5.0...
  Accept: application/json
  Authorization: Bearer token...
  Cookie: session=abc...
  (500-800 bytes per request)

Request 2: All headers repeated again
Request 3: All headers repeated again
...

HTTP/2 (HPACK Compression)

Request 1:
  :method: GET
  :path: /api/users
  :authority: api.example.com
  user-agent: Mozilla/5.0...
  (Indexed in static/dynamic table)

Request 2:
  :method: 2 (index reference)
  :path: 3  (index reference)
  ... (mostly indexes, ~50 bytes)

Savings: 80-90% reduction in header size

Stream Prioritization

HTTP/2 allows prioritizing critical resources.

Priority Levels

Critical (Highest):
- HTML document
- Critical CSS
- Fonts

High:
- Visible images
- Primary JavaScript

Medium:
- Non-critical JavaScript
- Below-fold images

Low:
- Analytics
- Ads
- Tracking scripts

Implementation

// Chromium priority hints
<link rel="preload" href="/critical.css" as="style" importance="high" />
<link rel="preload" href="/analytics.js" as="script" importance="low" />

<img src="/hero.jpg" importance="high" />
<img src="/footer-logo.jpg" importance="low" />

<script src="/app.js" importance="high"></script>
<script src="/analytics.js" importance="low"></script>

Enabling HTTP/2

Nginx

server {
  listen 443 ssl http2;
  
  server_name example.com;
  
  ssl_certificate /path/to/cert.pem;
  ssl_certificate_key /path/to/key.pem;
  
  # HTTP/2 push
  http2_push_preload on;
  
  location / {
    root /var/www/html;
    index index.html;
  }
}

Apache

# Requires mod_http2
LoadModule http2_module modules/mod_http2.so

<VirtualHost *:443>
  ServerName example.com
  
  # Enable HTTP/2
  Protocols h2 http/1.1
  
  SSLEngine on
  SSLCertificateFile /path/to/cert.pem
  SSLCertificateKeyFile /path/to/key.pem
</VirtualHost>

Node.js

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert'),
  allowHTTP1: true,  // Fallback to HTTP/1.1
});

server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200,
  });
  
  stream.end('<html><body>Hello HTTP/2!</body></html>');
});

server.listen(443);

Cloudflare

HTTP/2 is enabled by default on Cloudflare (free plan included).

HTTP/2 Best Practices

1. Don't Concatenate Everything

// ❌ HTTP/1.1 approach (bad for HTTP/2)
import './a.js';
import './b.js';
import './c.js';
// Webpack bundles into bundle.js (500 KB)

// ✅ HTTP/2 approach (split into chunks)
// Webpack code splitting
const A = lazy(() => import('./a.js'));  // 50 KB
const B = lazy(() => import('./b.js'));  // 80 KB
const C = lazy(() => import('./c.js'));  // 30 KB

2. Split by Route/Feature

// next.config.js
module.exports = {
  webpack: (config) => {
    config.optimization.splitChunks = {
      chunks: 'all',
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        home: {
          test: /[\\/]pages[\\/]index/,
          name: 'home',
          priority: 10,
        },
        dashboard: {
          test: /[\\/]pages[\\/]dashboard/,
          name: 'dashboard',
          priority: 10,
        },
      },
    };
    return config;
  },
};

3. Push Sparingly

Only push truly critical resources that are:

  • Small (< 50 KB)
  • Guaranteed to be needed
  • Not likely cached
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <link
          rel="preload"
          href="/css/critical.css"
          as="style"
        />
        <link
          rel="preload"
          href="/fonts/inter-var.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

5. Enable HTTP/3 (QUIC)

HTTP/3 builds on HTTP/2 with even better performance:

# Nginx (with QUIC support)
listen 443 ssl http3;
listen 443 ssl http2;

add_header Alt-Svc 'h3=":443"; ma=86400';

Domain Sharding (Don't Do This with HTTP/2!)

❌ HTTP/1.1 optimization (bad for HTTP/2):
static1.example.com
static2.example.com
static3.example.com
static4.example.com

✅ HTTP/2 optimization:
example.com (single domain)

Reason: HTTP/2 multiplexing makes multiple domains slower (extra DNS, TCP, TLS)

Testing HTTP/2

Check if HTTP/2 is Enabled

# cURL
curl -I --http2 https://example.com

# Should see:
HTTP/2 200

# Check protocol in browser DevTools
Network tab Protocol column h2

Verify Server Push

# Chrome DevTools
Network tab Look for "Push / " in Initiator column

Test Performance

// Measure time to load all resources
performance.getEntriesByType('resource').forEach(entry => {
  console.log(entry.name, entry.nextHopProtocol);  // 'h2' for HTTP/2
});

HTTP/2 vs HTTP/3

HTTP/2:
- Uses TCP
- Head-of-line blocking at TCP level
- 95%+ support

HTTP/3 (QUIC):
- Uses UDP
- No head-of-line blocking
- Faster connection establishment
- Better for mobile (connection migration)
- 75%+ support (growing)

Best Practices Summary

  1. Enable HTTP/2: Everywhere (it's 2024!)
  2. Split Code: Smaller chunks, better caching
  3. Single Domain: No domain sharding
  4. Server Push: Only critical resources
  5. Prioritization: Use importance hints
  6. Monitor: Check protocol in DevTools
  7. HTTP/3: Enable if supported
  8. TLS Required: HTTP/2 requires HTTPS

Common Pitfalls

Bundling everything: Defeats HTTP/2 benefits
Code splitting by route/feature

Domain sharding: Slower with HTTP/2
Single domain

Pushing everything: Wastes bandwidth
Push only critical, small resources

No prioritization: Everything loads equally
Use importance hints

HTTP/2 is a game-changer—use it properly and see dramatic performance improvements!

On this page