← Back to list

Website Performance Optimization (Part — 3): Network Optimization

Critical Rendering Path (CRP)

Ayush Verma in Towards Dev · 2026-06-05 20:56 · 50 claps · 12.1 min read paywalled
#website-performance #website-optimization #critical-rendering-path #frontend-development #network-optimization
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Website Performance Optimization (Part — 3): Network Optimization

Critical Rendering Path (CRP)

The Critical Rendering Path is the sequence of steps the browser must complete before it can display anything on screen.

  • As soon as the browser receives the first HTML packets, it starts building the DOM (Document Object Model)
  • While building the DOM, it waits for more packets — only when it has enough to construct something meaningful does it paint to the screen
  • The first packet from the server is always 14KB — this is a TCP constraint. Whatever fits in that first 14KB is what the browser has to work with immediately
  • The goal: get a meaningful FCP within that first 14KB payload — inline the bare minimum HTML + CSS needed to render something visible. Everything else loads in parallel

How CSS and JS affect CRP:

  • CSS is render-blocking — the browser will not paint anything until all CSS is downloaded and parsed. It needs the full CSSOM before it can render
  • JS is parser-blocking — when the browser encounters a <script> tag, HTML parsing stops completely until the JS is downloaded, parsed, and executed
  • This is why the order and placement of CSS and JS in your HTML matters enormously

Real example — Google.com:

  • Open google.com → right-click → View Source
  • Notice how maximum critical content is inlined directly in the HTML — styles, minimal JS
  • Build pipelines (Webpack, Vite) use inline injection plugins that automatically inline critical CSS into the HTML so the first paint happens with zero additional requests

Minimize HTTP Requests

Every HTTP request has overhead — it’s not just about file size.

Why fewer requests matter:

  • Each request requires a full connection setup — DNS lookup, TCP handshake, SSL negotiation — before a single byte of content arrives
  • Browsers have a limit of 6–10 parallel connections per domain on HTTP/1.1. Beyond that, requests queue and wait
  • 1 large data chunk is more efficient than 10 small chunks because you pay the connection overhead only once

Solutions:

  • Inline critical CSS — instead of <link rel="stylesheet" href="styles.css">, inject the above-the-fold styles directly in <style> tags in the HTML
  • Inline critical JS — small, essential scripts that gate rendering should be inlined, not external
  • Base64 for small images — encode tiny icons directly in CSS or HTML, eliminating a separate image request
  • SVG for icons and illustrations — SVGs can be inlined directly in HTML, zero extra requests, and scale perfectly

Practical example:

Without optimization — 6 separate requests:

index.html
index.css
product1.png
product2.png
product3.png
analytics.js

With optimization — 1 request:

index.html (with CSS inlined, SVGs inlined, critical JS inlined)

Result: FCP and LCP happen almost simultaneously because the browser has everything it needs in the first HTML response

Async Loading of JS: async vs defer

JS is parser-blocking by default. Here’s exactly what each approach does:

Default <script> in <head>

  • HTML parsing begins → browser hits <script>HTML parsing stops
  • Browser downloads the JS file → parses it → executes it → only then resumes HTML parsing
  • Worst case for performance — user sees a blank screen until JS is done
<head>
  <script src="app.js"></script> <!-- blocks everything -->
</head>

**async**

  • HTML parsing begins → browser downloads JS in parallel (doesn’t block HTML parsing during download)
  • But the moment the JS file finishes downloading → HTML parsing pauses while JS executes
  • Then HTML parsing resumes
  • Use for: independent scripts that don’t depend on the DOM or other scripts — analytics, ads
<script src="analytics.js" async></script>

**defer**

  • HTML parsing begins → browser downloads JS in parallel (same as async during download)
  • But JS execution is deferred until after the full HTML is parsed
  • Scripts execute in order, after DOM is ready
  • Use for: most application scripts — anything that needs the DOM to be ready
<script src="app.js" defer></script>

Visual summary:

Rule of thumb: Use defer for almost everything. Use async only for fully independent third-party scripts.

Avoid Redirects

Every redirect adds a full round trip before the browser gets to the actual page.

  • The most common culprit: HTTP → HTTPS redirect
  • Example: user types http://flipkart.com → server responds with 301 → browser makes a second request to https://flipkart.com → page finally loads
  • That redirect costs 100–300ms on a good connection, much more on mobile

Fix: hstspreload.org

  • HSTS (HTTP Strict Transport Security) tells browsers to never make an HTTP request to your domain — always upgrade to HTTPS automatically in the browser, before hitting the server
  • Submit your domain to hstspreload.org to be hardcoded into browsers’ HSTS preload list
  • Result: the HTTP → HTTPS redirect never happens at the network level — the browser goes straight to HTTPS
  • You can verify this on Google: open DevTools Network tab, visit http://google.com — it never actually makes an HTTP request, the browser upgrades it locally
Request URL: http://www.google.com/
Status Code: 307 Internal Redirect

This is a network request shown in Chrome DevTools (Network tab). It means:

  • You requested http:// (insecure) Google
  • The browser issued a 307 Internal Redirect — "internal" means the browser itself redirected you (not Google's server), likely due to HSTS (HTTP Strict Transport Security), which forces https:// before the request even leaves your machine.

Resource Hinting

Resource hints tell the browser about resources it will need soon — so it can start work on them early, before it would otherwise discover them.

The problem resource hints solve:

Resources like fonts, cross-origin JS, and images loaded after API responses are discovered late — only after CSS files are parsed, or after JS executes. By then, the browser has wasted time doing nothing when it could have been setting up connections.

1. preconnect

  • Tells the browser: “I will need resources from this origin soon — establish the connection now”
  • Sets up DNS lookup + TCP connection + SSL handshake in advance — so when the actual request comes, it goes straight to downloading
  • Use for: cross-origin servers you’re certain you’ll need — CDNs, font servers, API domains
<link rel="preconnect" href="https://cdn.glitch.global" crossorigin />

Example: Your Google Fonts CSS is loaded mid-parse. Without preconnect, the browser discovers fonts.googleapis.com at 800ms and spends 200ms on connection setup. With preconnect declared in <head>, that 200ms happens at page start in parallel — fonts arrive 200ms earlier

2. dns-prefetch

  • A lighter version of preconnect — only does the DNS lookup in advance, not the full connection
  • Lower resource cost — safe to use for origins you might need but aren’t certain about
  • Good for: third-party domains whose resources may or may not load depending on user behaviour
<link rel="dns-prefetch" href="https://cdn.glitch.global" />

Rule of thumb: Use preconnect for critical origins (fonts, main CDN). Use dns-prefetch for optional or uncertain origins (analytics, A/B testing tools)

3. preload

  • The most powerful hint — doesn’t just connect, it fetches the actual resource in advance
  • Resource is downloaded with high priority and cached, ready to use when needed
  • Use for: fonts you’ll definitely use, hero images, critical CSS/JS files that are discovered late
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero-image.webp" as="image" />
  • The as attribute is critical — it tells the browser what type of resource it is so it assigns the right fetch priority
  • Preloaded resources download in parallel with HTML parsing, not after

Example: Your hero font is defined in styles.css. Without preload, the browser discovers it only after CSS is parsed — causing a flash of invisible text (FOIT). With preload in <head>, the font is already downloaded before CSS even starts parsing

4. prefetch

  • Fetches resources needed for future navigation at low priority — doesn’t affect current page performance
  • Downloaded in the browser’s idle time and stored in cache
  • Use for: JS bundles for the next page the user is likely to visit
<link rel="prefetch" href="/checkout.js" />

Example: On a product detail page, prefetch the checkout page bundle. By the time the user clicks “Buy Now”, the JS is already in cache — next page loads instantly

5. prerender

  • The most aggressive hint — loads an entire page (HTML + all its dependencies) in the background with display: hidden
  • Page is fully rendered and waiting in cache — navigation feels instant
  • Use sparingly — it consumes significant memory and bandwidth for a page the user may never visit

Example: On a multi-step form, prerender step 2 while the user is filling step 1. When they click Next, step 2 appears instantly

Fetch Priority

When you don’t explicitly set priority, the browser guesses — and it often guesses wrong, treating everything as high priority.

How fetchpriority works

  • By default, browsers assign fetch priority based on resource type and position in the document
  • fetchpriority="high" — tell the browser this is critical, fetch it first
  • fetchpriority="low" — tell the browser this can wait, don't compete with critical resources
  • fetchpriority="auto" — default browser behaviour

Preloading scripts with priority control

<!-- Critical script — high priority (default) -->
<link rel="preload" href="critical-script.js" as="script" />

<!-- Non-critical preloaded script - explicitly lowered -->
<link rel="preload" href="/js/script.js" as="script" fetchpriority="low" />

Without fetchpriority="low", the browser sees a preload tag and immediately treats it as high priority — competing with your LCP image or critical CSS.

Preloading CSS without blocking rendering

<link rel="preload" as="style" href="theme.css" 
      fetchpriority="low" 
      onload="this.rel='stylesheet'">

This is a clever pattern:

  • rel="preload" tells the browser to download the CSS file in the background
  • fetchpriority="low" ensures it doesn't compete with above-the-fold resources
  • onload="this.rel='stylesheet'" — once downloaded, the rel attribute switches to stylesheet, triggering CSS parsing and applying styles
  • Result: CSS downloads without blocking HTML parsing, then applies instantly when ready

Image carousel example

<ul class="carousel">
  <img src="img/carousel-1.jpg" fetchpriority="high">  <!-- visible first -->
  <img src="img/carousel-2.jpg" fetchpriority="low">   <!-- offscreen -->
  <img src="img/carousel-3.jpg" fetchpriority="low">   <!-- offscreen -->
  <img src="img/carousel-4.jpg" fetchpriority="low">   <!-- offscreen -->
</ul>
  • Only the first image is visible on load — it directly impacts LCP
  • Images 2–4 are offscreen, hidden behind carousel navigation
  • Without fetchpriority="low", all four images compete equally for bandwidth
  • With explicit priority, the browser downloads image 1 first at full speed, then downloads 2–4 lazily in the background
  • Real impact: LCP can improve by 200–400ms on image-heavy pages

Refer: https://web.dev/articles/fetch-priority

Early Hints (103)

The problem

The browser sends a request → the server spends time processing (database queries, authentication, rendering) → only then does it respond with HTML. During all that server processing time, the browser is sitting idle — it could have been warming up connections or downloading resources.

What Early Hints solves

  • The server sends a 103 Early Hints response before the final 200 OK response
  • This interim response tells the browser: “I’m still working on your page, but here’s what you’ll need”
  • The browser immediately starts preconnecting or preloading those resources while the server finishes processing
  • By the time the 200 OK arrives with the full HTML, resources are already downloading or connections are already warm
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script

[server continues processing...]
HTTP/1.1 200 OK
Content-Type: text/html

Real-world impact

  • Shopify reported a ~100ms improvement in LCP after enabling Early Hints
  • The gain equals roughly the server think time — all that dead time is now used productively
  • Requires server support — Cloudflare, Nginx 1.25+, and CDNs like Fastly support it natively

Refer: https://developer.chrome.com/docs/web-platform/early-hints

HTTP Upgrade: 1.1 vs HTTP/2 vs HTTP/3

HTTP/1.1 — The old bottleneck

  • Maximum 6 parallel connections per domain — if your page has 30 resources, requests queue and wait
  • No streaming — each request is a full round trip
  • Headers sent as plain text on every request — repeated, uncompressed
  • No server push — browser must discover and request every resource individually
  • Example: open Flipkart in DevTools → Network tab → you’ll see resources queuing in groups of 6, a classic HTTP/1.1 waterfall

HTTP/2 — The modern standard

  • Multiplexing — all requests over a single connection simultaneously, no queuing
  • Header compression (HPACK) — repeated headers are compressed, saving bandwidth on every request
  • Server Push — server can proactively send JS and CSS alongside the HTML response, before the browser asks for them
  • Stream prioritization — server can send critical resources first
  • Flow control — prevents fast senders from overwhelming slow receivers
  • HTTP/2 requires HTTPS
  • Example: on an HTTP/2 server, all 30 resources start downloading immediately in parallel — the waterfall is almost flat

HTTP/3 — The streaming future

  • Drops TCP entirely, uses UDP — no three-way handshake, no acknowledgement overhead
  • This eliminates head-of-line blocking — in TCP, a lost packet stalls everything behind it; in UDP, other streams continue unaffected
  • Built-in TLS 1.3 — encryption is part of the protocol, not a separate layer
  • Uses QUIC (Quick UDP Internet Connections) — Google’s protocol, now standardised
  • All HTTP/2 capabilities (multiplexing, header compression, server push, prioritization) plus UDP speed
  • HTTP/3 requires HTTPS
  • Used by: YouTube, Google Search, Facebook — anywhere low-latency streaming matters

Comparison table

Reality check: HTTP/2 is what most of the web runs on today. HTTP/3 adoption is growing — YouTube and major streaming platforms use it. Check your site with Chrome DevTools Protocol column in the Network tab.

Compression: Brotli vs Gzip

Every text file (HTML, CSS, JS) your server sends gets compressed before transmission and decompressed by the browser. Smaller files = faster downloads = better performance.

Brotli — the modern choice

Developed by Google in 2015, Brotli uses a pre-trained dictionary of common web patterns:

  • 14% smaller than Gzip for JavaScript
  • 21% smaller than Gzip for HTML
  • 17% smaller than Gzip for CSS
  • Supported by all modern browsers — check via Accept-Encoding: br in request headers
  • Server responds with Content-Encoding: br

Two approaches to compression

Runtime compression — compress on the fly as requests come in:

// Node.js — shrink-ray middleware
const shrinkRay = require('shrink-ray-current');
app.use(shrinkRay());

Simple to set up, but adds CPU overhead per request. Fine for low-traffic servers.

Build-time compression — pre-compress assets during your build:

// webpack — brotli-webpack-plugin
const BrotliPlugin = require('brotli-webpack-plugin');

module.exports = {
  plugins: [
    new BrotliPlugin({
      asset: '[path].br[query]',
      test: /\.(js|css|html|svg)$/,
    })
  ]
}

Assets are compressed once at build time and served as static .br files. Zero runtime CPU cost — the preferred approach for production. Your CDN serves app.js.br directly.

Refer: https://web.dev/articles/codelab-text-compression-brotli

HTTP Caching

Caching means: instead of re-downloading an unchanged file, serve it from the browser’s local cache. For repeat visitors, a well-cached site loads almost instantly.

Cache-Control header — the main control knob

Cache-Control: max-age=31536000, immutable

Key directives:

  • **max-age=N** — cache this resource for N seconds. max-age=31536000 = 1 year (use for hashed assets like app.abc123.js)
  • **no-cache** — always revalidate with server before using cached version (not the same as "don't cache")
  • **no-store** — never cache (for sensitive data like banking pages)
  • **immutable** — tells the browser the file will never change; don't even revalidate on reload
  • **stale-while-revalidate=N** — serve stale cache immediately, revalidate in background for next visit

Supporting headers

  • **ETag** — a fingerprint of the file's content. Browser sends If-None-Match: <etag> on next request; if unchanged, server returns 304 Not Modified with no body — saving bandwidth
  • **Last-Modified** — timestamp of last change. Browser sends If-Modified-Since header; server returns 304 if unchanged
  • **Expires** — older alternative to max-age, sets an absolute expiry date

Caching strategy by asset type

Service Worker Caching

Service workers intercept network requests and serve responses from a local cache — enabling offline support and instant repeat loads.

// sw.js — install event: cache critical assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/scripts/app.js',
      ]);
    })
  );
});

// fetch event: cache-first strategy
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request);
    })
  );
});

Common caching strategies

  • Cache-first — serve from cache if available, fall back to network. Best for static assets that rarely change
  • Network-first — always try network, fall back to cache if offline. Best for API data
  • Stale-while-revalidate — serve cache immediately, update cache in background. Best balance of speed and freshness

What service worker caching unlocks

  • Offline support — pages load even with no internet connection
  • Instant repeat visits — no network round trip for cached assets
  • Background sync — queue user actions (form submissions, etc.) while offline, replay when back online
  • PWA foundation — service workers are what make Progressive Web Apps installable and offline-capable

The key difference from HTTP caching: HTTP cache is controlled by response headers and managed by the browser. Service worker cache is controlled by your JavaScript — you decide exactly what gets cached, for how long, and with what strategy.

What’s next:

If this article helped you think differently about web performance, share it with your team. The best performance wins happen when the entire team — not just one engineer — understands why it matters.


메타데이터
post_id
61dc43fad89d
slug
website-performance-optimization-part-3-network-optimization-61dc43fad89d
url
https://towardsdev.com/website-performance-optimization-part-3-network-optimization-61dc43fad89d
canonical_url
https://towardsdev.com/website-performance-optimization-part-3-network-optimization-61dc43fad89d
author_url
https://medium.com/@ayushv
status
ok
fetched_at
2026-06-10 21:21:38