How to Fix a Slow Website: A Practical CDN and Caching Guide

Fixing a slow website: locate the bottleneck with TTFB, then decide on CDN and cache headers — plus what compression and HTTP/2 buy you.

2026-09-12

Fixing a slow website: locate the bottleneck with TTFB, then decide on CDN and cache headers — plus what compression and HTTP/2 buy you.

A slow website is the most common and most frequently misdiagnosed performance problem. The delay can sit in DNS, TTFB, image weight, JavaScript execution — or simply in misconfigured cache headers. This guide follows the real diagnostic order: locate the bottleneck with a command, then decide whether you need a CDN, how long to cache, and how hard to compress images — rather than buying a CDN first and guessing afterwards.

Order of judgement: TTFB first, transfer weight second, render blocking third. A CDN only fixes part of the first two; the third is a code and build problem.

Locate the bottleneck first

Split a page load into four stages and the problem usually reveals itself: connection setup (DNS + TCP + TLS), time to first byte, transfer (HTML / CSS / JS / images), and parse and render (JS execution, layout, paint). Each stage needs a different fix, and reaching for the wrong tool is how teams waste money.

  • TTFB above 600ms: the bottleneck is the backend or the region. Look at a CDN and a closer origin
  • TTFB under 200ms but slow first paint: the bottleneck is asset weight and blocking. Compress images, cut requests
  • Fast repeat visits, slow first visit: caching already works — optimise the cold path instead (warm-up, inline critical CSS)

One command gives you the four numbers you need:

curl -s -o /dev/null -w "dns:%{time_namelookup}s tcp:%{time_connect}s ttfb:%{time_starttransfer}s total:%{time_total}s\n" https://example.com/ time_starttransfer is TTFB. Re-run with -H 'Cache-Control: no-cache': if TTFB jumps noticeably, the earlier number was a CDN cache hit rather than the origin's real response time.

What a CDN actually does: edge PoPs, reverse proxy, cache hits

A CDN is fundamentally a reverse proxy plus a cache, placed next to your users. It keeps cacheable content on edge nodes (PoPs) in major cities and inside ISPs, so requests are served nearby instead of travelling back to origin every time. How to compare coverage, billing granularity and origin-fetch rules: CDN acceleration.

  • Lower latency: 5–30ms to a nearby edge node replaces a 100–200ms round trip to origin
  • Cache hits: nothing goes back to origin; static-asset hit ratios commonly sit at 80–95% in practice
  • Origin offload: bandwidth and connection pressure move to the CDN; keep only fallback capacity on origin
  • Protocol upgrades: most CDNs enable HTTP/2, HTTP/3 and TLS 1.3 for you

Confirming a hit: read the response header. Cloudflare uses cf-cache-status: HIT, AWS CloudFront uses x-cache: Hit from cloudfront, Fastly uses X-Cache: HIT, and Alibaba DCDN / Tencent Cloud CDN typically use X-Cache. Field names differ per vendor — run curl -sI https://your-domain/ once before drawing conclusions.

On a cache miss, a CDN can be marginally slower than hitting origin directly, because of the extra hop. That is why the rules must be paired: long TTL for static assets, short TTL for HTML. Long-cached HTML is the most common way a CDN rollout goes wrong.

TTFB caps your first paint and your conversion rate

TTFB (Time To First Byte) is the gap between sending a request and receiving the first byte. It sets when HTML starts downloading, and therefore largely sets the floor for LCP — in practice TTFB often accounts for more than half of LCP. Google treats TTFB under 800ms as good, while Core Web Vitals require LCP under 2.5s; spend more than that and the window left for rendering is simply gone.

The more practical issue is revenue. In Google and SOASTA's large retail performance study, every 1% drop in mobile page speed correlated with roughly a 1% drop in sales; in an A/A test that deliberately sped pages up, a 20% faster first paint corresponded to about a 9% lift in conversion. Those figures come from a specific sample and are not your forecast — but the direction and magnitude are enough to make the point: TTFB is a revenue line, not a matter of tidiness.

Realistic targets and levers:

  • With CDN hits, hold TTFB under 200ms
  • Place the origin in a region close to your users — every 100ms of cross-border round trip saved reads to users as "this page feels faster"
  • Add page-level or reverse-proxy caching to avoid repeated database queries
  • Move anything not needed for first paint (recommendations, comments, third-party scripts) to async loading

Did it actually get faster? If TTFB dropped from 150ms to 30ms but total time did not move, the bottleneck has moved to transfer weight or rendering — stop tuning cache headers and go work on images and JavaScript.

Cache-Control: put the policy in the response header

Browsers and CDNs only read headers. If your caching policy is not in Cache-Control, the default behaviour is to revalidate on every request, which means caching is effectively off. These four patterns cover most cases.

Hashed JS / CSS Cache-Control: public, max-age=31536000, immutable HTML pages Cache-Control: public, s-maxage=60, stale-while-revalidate=300 Versioned images Cache-Control: public, max-age=2592000 Logged-in pages / personal data Cache-Control: private, no-store

  • max-age is for browsers; s-maxage is for shared caches such as CDNs and takes precedence
  • no-cache does not mean no caching: it may be stored but must always be revalidated (a 304 with no body is fine)
  • no-store means never write to any cache — the only correct choice for personal data
  • immutable skips revalidation even on refresh; only safe when the file URL carries a hash
  • stale-while-revalidate serves the stale copy while refreshing in the background — strongly recommended for HTML

Compression only works if you also send Vary: Accept-Encoding, otherwise the CDN may hand a brotli body to a client that cannot decode it, or the gzip body to everyone.

Choosing TTL: static versus dynamic content

TTL is a two-sided trade-off: staleness (how long before an update is visible) versus origin traffic (how many requests reach your servers). Static assets get a long TTL and updates move into the filename hash; dynamic content trades freshness for a short TTL. Moving static files off the app server into object storage and putting the CDN in front is usually the easiest win here — storage tiers and lifecycle rules: AWS S3 storage tiers.

Content typeSuggested Cache-ControlTTLNotes
Hashed JS / CSSpublic, max-age=31536000, immutable1 yearChange content, change filename — safe to cache hard
Versioned images / fontspublic, max-age=259200030 daysRotate the URL or filename to bust it
HTML pages (home, articles)public, s-maxage=60, stale-while-revalidate=30060s – 5minEffectively visible right after deploy
Public GET APIs (lists, quotes)public, s-maxage=5-305–30sSet by how stale the data may be
Authenticated APIs / cart / ordersprivate, no-storeNot cachedAny shared-cache hit here is a data leak
Logged-in HTMLprivate, no-cacheRevalidate each timeStops one user seeing another user's page
Third-party scripts and fontsVendor default—Self-host anything on the critical path

Start dynamic content at 5–30 seconds, not zero: watch how often the data actually changes. If an endpoint updates a few times a day, a 60-second TTL is fine and cuts origin traffic by an order of magnitude.

How browser, CDN and origin caches divide the work

A request passes through three layers, and solving it at the earliest layer is always cheaper. Each layer is configured and invalidated independently.

  1. Browser cache: governed by max-age; repeat visits make zero network requests (shown as memory cache / disk cache in DevTools)
  2. CDN cache: governed by s-maxage; a hit never reaches origin and is where you save origin bandwidth
  3. Origin cache (Nginx or application page cache): the last line of defence, and what actually sets TTFB when the CDN misses

When the CDN does not cache — or after a purge — browser and origin fall back to conditional requests: an ETag (or Last-Modified) with If-None-Match gets a 304 Not Modified back when nothing changed, and no body is transferred. Conditional caching saves bandwidth and time; it does not reduce the number of requests.

Never cache HTML for long. Serving stale markup after a deploy is worse than being slow: broken features and 404s on links. HTML TTLs should be as short as the business can tolerate.

Compression and images: saved bytes are saved time

Transfer time scales with bytes. On 10 Mbps, 1MB takes about 0.8 seconds and 300KB about 0.25 seconds — not theory, multiplied straight into first paint.

  • Text compression: gzip typically cuts HTML / CSS / JS by 60–80%; brotli is a further 15–25% smaller than gzip at comparable quality
  • Image formats: WebP commonly saves 25–34% over JPEG, and AVIF commonly saves around 50% over JPEG
  • Right size per screen: serve 2× images to 2× displays with srcset + sizes instead of one large file for everyone
  • Lazy load: add loading="lazy" below the fold — but never to the LCP image
  • Reserve dimensions: set width / height on images to keep CLS at or below 0.1

The LCP image needs special handling: add fetchpriority="high" or <link rel="preload" as="image"> to get the request started early, and exclude it from lazy loading. It is the largest first-paint asset; if it loses the request queue, better compression buys you nothing.

Most CDNs can convert to WebP / AVIF based on the Accept request header and compress with brotli at the edge. Before enabling it, check two things: whether your vendor supports it, and whether both formats share the same cache key — otherwise one URL caches two variants and your hit rate drops for free.

HTTP/2 and HTTP/3: what connection reuse is worth

A page with 40 static assets typically opens six connections and repeats TCP plus TLS handshakes over HTTP/1.1. HTTP/2 multiplexes them over a single connection with HPACK header compression, so one handshake covers the whole page and real-world tests usually save 100–300ms of first paint.

  • Enable H2 on both CDN and origin. CDN-only H2 still sends the edge-to-origin leg over HTTP/1.1 and halves the benefit
  • Stop bundling everything into one giant file and stop sharding domains across subdomains — those were HTTP/1.1 workarounds and now slow parsing
  • HTTP/3 runs over UDP/QUIC: it supports 0-RTT resumption and better loss recovery, which is a visible win on mobile networks

Roll out HTTP/3 gradually. Some enterprise firewalls, older carrier networks and middleboxes drop UDP. Verify the handshake with curl --http3 -s -o /dev/null -w "%{http_version} %{time_starttransfer}s\n" https://example.com/, and make sure failure falls back to HTTP/2 automatically.

Invalidating caches when content changes

Long TTLs mean long-lived staleness. Three approaches, in descending order of reliability.

  1. Versioned URLs (preferred): put a content hash in the filename or query string, e.g. app.4f3a2b.js. New content is a new URL, so no purge is needed and hit rates are unaffected
  2. Purge by URL or prefix: use this only when the URL must stay stable. A purge request from your deploy pipeline usually propagates globally within seconds to tens of seconds
  3. Warm-up: after a deploy, pull the new version back to the edge explicitly so a cold start does not stampede origin

How fast each approach lands: versioned URLs are live on first request with no action at all; a per-URL purge typically reaches the whole network within seconds to tens of seconds, though rate limits vary by API; warm-up pulls the new version forward on purpose, which is the cheapest option but costs you one extra step in the pipeline.

Purging is not free: a full purge causes an origin thundering herd. On a high-traffic site run it during a quiet window, or configure rate limiting plus automatic warm-up. Re-check with curl -sI right after purging to confirm the new version is being served before you ship anything else.

Measuring it: Lighthouse, WebPageTest and a TTFB baseline

Optimisation requires a baseline. Without a before/after comparison every performance claim is guesswork. Three tools, three jobs:

  • Lighthouse (Chrome DevTools → Lighthouse, or npx lighthouse https://example.com --only-categories=performance): reports LCP, CLS, TBT and the "reduce initial server response time" audit
  • WebPageTest: multi-location TTFB and First Contentful Paint, with First View separated from Repeat View so you can see whether the CDN is working
  • DevTools Network waterfall: read each row's Size column, 200 / 304 status, Priority, and the memory cache / disk cache markers
MetricGoodPoorTool
TTFB≤ 0.8s (≤ 0.2s with CDN hits)> 1.8sWebPageTest / curl
LCP≤ 2.5s> 4.0sLighthouse / CrUX
INP≤ 200ms> 500msLighthouse / RUM
CLS≤ 0.1> 0.25Lighthouse

Lab data is not field data. Office networks are fast and quiet, which flatters everything. Where budget allows, add RUM or at least re-check from multiple WebPageTest locations, so you do not optimise only for the test tool.

A one-week optimisation checklist

Work in this order and you should see measurable movement in TTFB and first paint within a week. Every step has a number attached to it.

  1. Day 1 — baseline: record DNS / TCP / TTFB / total with the curl command above, then run Lighthouse and file all four numbers
  2. Day 2 — diagnose: identify every endpoint above 600ms TTFB and the five heaviest assets; ignore everything else for now
  3. Day 3 — cache headers: give hashed JS / CSS a one-year immutable TTL, HTML s-maxage=60 plus stale-while-revalidate, and confirm Vary: Accept-Encoding
  4. Day 4 — CDN: onboard static assets only, verify the X-Cache header and TTFB under 200ms, then consider HTML
  5. Day 5 — compression and images: enable brotli, automatic WebP / AVIF conversion, and give the LCP image fetchpriority="high" with no lazy load
  6. Days 6–7 — protocols and recheck: confirm H2 on CDN and origin, canary HTTP/3, then re-run Lighthouse and WebPageTest against the baseline

Next step: start with the first three items — baseline plus cache headers. That needs no new purchase and usually removes a large share of repeated requests. If TTFB is still high after that, evaluate a CDN and a closer origin region — and bring the three numbers from the curl command (TTFB / total / whether it was a cache hit) as a table, which makes comparing proposals far more efficient.

Want to price this for your own usage?

Tell us your region and spec — an engineer will size it and quote. We accept USDT TRC-20.

Top up now