HTTP-välimuistin hallinta 2026: Cache-Control, stale-while-revalidate ja edge-strategiat

Miten hallita HTTP-välimuistia oikein 2026: Cache-Control-direktiivit, stale-while-revalidate ja CDN-invalidointi Cloudflaressa, Fastlyssa ja Vercelissä. Kaavat, koodit ja mittausohjeet.

Päivitetty: 15. elokuuta 2026

HTTP-välimuistin hallinta tarkoittaa Cache-Control-otsikon, stale-while-revalidate-direktiivin ja CDN-tason invalidoinnin yhdistämistä siten, että selain ja reunasolmut palvelevat sisältöä ilman turhaa origin-käyntiä. Käytännössä oikea kaava on lähes aina public, max-age=0, s-maxage=31536000, stale-while-revalidate=86400 HTML:lle ja public, max-age=31536000, immutable hashatuille resursseille. Käydään tässä oppaassa läpi vuoden 2026 työkalut (RFC 9111, Cache-Status, Next.js 15 revalidateTag sekä Cloudflare-, Fastly- ja Vercel-erot), jotta TTFB tippuu ilman että käyttäjät näkevät vanhaa sisältöä.

  • RFC 9111 (2022) on nykyinen HTTP-välimuistin standardi ja korvaa RFC 7234:n. stale-while-revalidate tulee RFC 5861:sta ja on tuettu kaikissa isoissa CDN:issä vuonna 2026.
  • Käytä max-age selaimelle ja s-maxage jaetulle CDN-välimuistille. Nämä eivät ole sama asia, ja niiden erottaminen mahdollistaa "instant HTML + immutable assets" -kaavan.
  • stale-while-revalidate palvelee vanhaa sisältöä välittömästi ja päivittää taustalla, jolloin TTFB pysyy alle 50 ms myös cold cache -tilanteissa.
  • Reunavälimuistin invalidointi tag- tai surrogate-key-pohjaisesti (Cloudflare Cache Tags, Fastly Surrogate-Key, Vercel revalidateTag) on ainoa skaalautuva tapa hallita monimutkaista sisältöä.
  • Mittaa Cache-Status-otsikolla (RFC 9211) hit ratio, älä oleta. 90 %+ edge-hit-suhde on realistinen tavoite content-sivuille.
  • bfcache (selaimen back/forward-välimuisti) antaa lähes nollan LCP:n takaisin-navigoinnissa, mutta rikkoutuu no-store-otsikosta ja epäasianmukaisista unload-kuuntelijoista.

Mikä on HTTP-välimuisti ja miksi sillä on väliä 2026

HTTP-välimuisti on kerros palvelimesi ja käyttäjän välissä. Se tallentaa vastauksia ja palauttaa ne uudelleen ilman origin-pyyntöä. Kerros voi olla selain, service worker, yrityksen proxy, reunavälimuisti (CDN) tai kääntävä välipalvelin kuten Varnish. Kun kerrokset toimivat yhdessä, käyttäjän kokema latenssi tippuu satojen millisekuntien luokasta muutamiin, ja origin-palvelin selviää kymmenkertaisella liikenteellä.

Vuonna 2026 pelisäännöt tulevat RFC 9111:sta, joka päivitti HTTP/1.1-välimuistin määrittelyt vuonna 2022 ja on nyt kaikkien selainten ja CDN:ien lähtökohta. Käytännön muutokset RFC 7234:sta ovat pieniä mutta merkittäviä: Age-laskenta on tarkennettu, heuristic freshness on selkeämmin määritelty ja must-understand-direktiivi on lisätty. Kentältä havaittuna suurin muutos on kuitenkin Cache-Status-otsikko (RFC 9211), jota Cloudflare, Fastly ja Akamai lähettävät nyt oletuksena. Mittaaminen on vihdoin yksinkertaista.

Yhteys perf-metriikoihin on suora. Hyvin viritetty välimuisti kutistaa TTFB:n selaimen näkökulmasta lähelle nollaa (edge-hit) ja stabiloi LCP:tä, koska pääasiakirja saapuu välittömästi. Jos sinulla on vielä TTFB-ongelmia ennen välimuistiin siirtymistä, kannattaa lukea myös TTFB-optimoinnin käytännön opas. Välimuisti ei korjaa hidasta originia, se vain piilottaa sen.

Cache-Control-direktiivien pikakäsikirja

Cache-Control on ainoa otsikko, jonka tarvitset 95 % tapauksista. Expires, Pragma ja Last-Modified ovat legacy-kalustoa. RFC 9111 tukee niitä taaksepäinyhteensopivuuden vuoksi, mutta uutta koodia ei niille kirjoiteta. Alla ovat direktiivit, jotka pitää osata ulkoa:

  • max-age=N: kuinka monta sekuntia vastaus on tuore selaimen yksityisessä välimuistissa.
  • s-maxage=N: sama jaetuille välimuisteille (CDN, proxy). Ohittaa max-age-arvon jaetuissa cacheissa.
  • public / private: saako jaettu välimuisti tallentaa. Cookielliset ja henkilökohtaiset vastaukset merkitään private.
  • no-cache: saa tallentaa, mutta pitää validoida (ETag/Last-Modified) ennen käyttöä. Yleisin väärinymmärrys koko HTTP:ssä, koska tämä ei estä tallennusta.
  • no-store: ei saa tallentaa mihinkään. Käytä vain aidosti arkaluontoiselle datalle. Hajottaa myös bfcachen.
  • immutable: sisältö ei koskaan muutu tämän URLin takana. Selain ohittaa refresh-validoinnit F5:ää painettaessa. Käytä hashatuille asseteille.
  • stale-while-revalidate=N: palvelee vanhentunutta vastausta N sekuntia ja hakee tuoreen taustalla.
  • stale-if-error=N: sama, mutta vain kun origin epäonnistuu (5xx). Halpavakuutus outageille.
  • must-revalidate: vanhentunutta ei koskaan saa palvella. Käytä tapahtumasidonnaiselle datalle (varastosaldot, hinnat).

Tässä käyttökelpoinen kaava eri sisältötyypeille:

# HTML-sivu (Next.js SSR, WordPress-teema, Rails-render)
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400

# Hashattu JS/CSS/kuva (app.a1b2c3.js, hero.d4e5f6.avif)
Cache-Control: public, max-age=31536000, immutable

# API-vastaus, joka voi vanhentua nopeasti
Cache-Control: public, max-age=30, s-maxage=60, stale-while-revalidate=300

# Käyttäjäkohtainen dashboard-fragmentti
Cache-Control: private, no-cache

# Kirjautumiskutsu, maksutapahtuma
Cache-Control: no-store

Huomaa max-age=0, s-maxage=3600-kombo HTML:lle. Selain hakee tuoreen HTML:n joka navigoinnilla, mutta CDN palvelee samaa vastausta tunnin. Käyttäjä saa muutokset heti kun purge tehdään, ja origin näkee vain 1/N pyynnöistä.

stale-while-revalidate: nopea sivu ilman vanhentumista

stale-while-revalidate (SWR) on määritelty RFC 5861:ssa, ja se on vuoteen 2026 mennessä laajimmin tuettu perf-työkalu jaettujen välimuistien joukossa. Idea on yksinkertainen. Kun objekti vanhentuu (max-age ylittyy), CDN palauttaa vanhentuneen version heti ja lähettää samalla taustalla revalidointipyynnön origiiniin. Käyttäjän TTFB pysyy edge-tasoisena, ja seuraava vierailija saa jo tuoreen version.

Kaadoin itse tämän kaavan viime keväänä uutispalveluun, jossa etusivun HTML oli aiemmin miss-vetoinen joka minuutti. SWR-siirron jälkeen origin näki alle 1 % liikenteestä. Käytännön esimerkki blogialustalla, jossa artikkelien HTML voi elää tunnin ennen refreshiä mutta tuoreus 24 h sisällä on ok:

// Node.js / Express -esimerkki
app.get('/artikkeli/:slug', async (req, res) => {
  const article = await db.article.findBySlug(req.params.slug);

  res.set({
    'Cache-Control': 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400',
    'Content-Type': 'text/html; charset=utf-8',
    'ETag': article.etag,
  });

  res.send(renderArticle(article));
});

Mitä tässä tapahtuu ajan kanssa:

  • 0–3600 s: CDN palauttaa cached-vastauksen välittömästi. TTFB noin 15–30 ms.
  • 3600–90 000 s: CDN palauttaa vanhentuneen vastauksen (~15 ms) ja lähettää revalidoinnin origiiniin taustalla. Käyttäjä ei koskaan odota.
  • Yli 90 000 s: CDN pakotetaan hakemaan tuore vastaus. Kylmä TTFB, mutta harvoin.

SWR yhdessä stale-if-error:n kanssa on paras yksittäinen investointi outage-kestävyyteen. Kun origin kaatuu, CDN palvelee viimeisintä versiota stale-if-error-ikkunan ajan. Sivustosi pysyy pystyssä vaikka backend on nurin.

ETag ja ehdolliset pyynnöt: 304 säästää kaistaa

Kun Cache-Control vanhentuu, selain ei aina lataa koko vastausta uudelleen. Jos vastauksessa oli ETag (tai Last-Modified), selain lähettää revalidointipyynnön If-None-Match-otsikolla, ja palvelin vastaa 304 Not Modified ilman bodya, jos sisältö ei ole muuttunut. Tämä säästää sekä kaistaa että LCP-aikaa, koska selain käyttää olemassa olevaa vastausta.

# Ensimmäinen pyyntö
GET /artikkeli/http-valimuisti HTTP/1.1
Host: webperfclinic.com

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "v42-a1b2c3d4"
Content-Length: 24817
...html body...

# Vanhentumisen jälkeen selain lähettää:
GET /artikkeli/http-valimuisti HTTP/1.1
Host: webperfclinic.com
If-None-Match: "v42-a1b2c3d4"

# Jos ei muuttunut:
HTTP/1.1 304 Not Modified
ETag: "v42-a1b2c3d4"
Cache-Control: public, max-age=3600
# ei bodya, selain käyttää välimuistin versiota

Generoi ETag sisällöstä (esim. SHA1(body).slice(0, 16)) tai sisällön versiosta (v${article.version}-${article.updatedAt}). Älä käytä satunnaista arvoa, se pilaa koko mekaniikan. Muista myös Vary-otsikko, jos vastaus riippuu Accept-Encoding:sta tai Accept-Language:sta. Muuten CDN palvelee brotli-vastauksen asiakkaalle, joka pyysi gzipiä (osunut tähän itse tuotannossa, ei suositella).

Reunavälimuisti ja monitasoinen cache Cloudflaressa, Fastlyssa ja Vercelissä

Yksitasoinen välimuisti on vuonna 2026 harvinaisuus. Nykyaikainen stack näyttää tältä: selain → service worker → CDN edge PoP → CDN regional tier → origin shield → origin. Jokainen kerros noudattaa samoja Cache-Control-sääntöjä, mutta niiden konfigurointi ja invalidointi eroaa oleellisesti.

Ominaisuus Cloudflare Fastly Vercel
Invalidointimalli Cache Tags + URL purge Surrogate-Key + soft/hard purge revalidateTag / revalidatePath
Tag-purge-latenssi < 1 s globaalisti < 150 ms globaalisti < 300 ms edge-verkossa
SWR-tuki Kyllä (natiivi) Kyllä (VCL stale-while-revalidate) Kyllä (ISR + fetch-tags)
Tiered cache Argo / Tiered Cache Shielding Regional edge cache
Cache-Status-otsikko Oletuksena päällä Oletuksena päällä x-vercel-cache
Hinta 1 TB / kk ~$0 (Free/Pro) ~$120 Sisältyy pro-planiin

Origin shield tarkoittaa yhtä valittua PoPia, joka toimii origin-palvelimen etunaamana. Kaikki muut PoPit menevät sen läpi. Vaikutus on iso. Jos sivustosi liikenne jakautuu 250 PoPin kesken, ilman shieldiä jokainen niistä voi lähettää cold-miss-pyynnön originiin. Shieldillä origin näkee korkeintaan yhden pyynnön tuoreelle sisällölle per objekti.

Kolmannen osapuolen skriptien osalta CDN-strategia on eri, koska et hallitse niitä. Ne kannattaa proxytata oman domainisi kautta. Käsittelin tätä syvemmin kolmannen osapuolen skriptien optimointi -oppaassa.

Cache-tunnisteet ja purge: hallittu invalidointi

URL-pohjainen purge (PURGE /artikkeli/x) on toiminut 15 vuotta ja toimii yhä, mutta se ei skaalaudu monimutkaisiin riippuvuuksiin. Kuvitellaan tilanne: päivität kirjoittajan nimen, ja jokainen artikkelisivu, kategoriasivu ja RSS-feed pitää invalidoida. URL-purge vaatisi tuhansia kutsuja. Tag-purge yhden.

Kaava on yksinkertainen. Palvelin liittää vastaukseen yhden tai useamman tagin, ja invalidointi viittaa tagiin. Cloudflaressa (Enterprise):

# Palvelin lähettää artikkelin
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=86400
Cache-Tag: article-42, author-mateo, category-perf

# Invalidointi kun Mateo päivittää bion
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"tags":["author-mateo"]}'

Fastlyssa sama Surrogate-Key-otsikolla ja fastly purge --surrogate-key. Vercelissä revalidateTag('author-mateo'), joka käyttää Vercelin oman fetchin liittämiä tageja.

Suunnitteluperiaate: liitä jokaiseen cache-tavaraan tagit, jotka kuvaavat sen riippuvuuksia. Yksi kutsu tageilla on halvempi ja luotettavampi kuin sadan URL-purgen batch-ajo.

Next.js 15 ISR ja revalidateTag käytännössä

Next.js 15 (marraskuu 2024, edelleen aktiivinen 2026) rakentaa ISR:n (Incremental Static Regeneration) suoraan HTTP-välimuistin päälle. Fetch-kutsuun liitetään tagit, ja server action tai webhook kutsuu revalidateTag:iä muutosten yhteydessä. Alla toimiva minimi:

// app/artikkeli/[slug]/page.tsx
export const revalidate = 3600; // s-maxage=3600 alustavaisena raja

async function getArticle(slug: string) {
  const res = await fetch(`https://api.example.com/articles/${slug}`, {
    next: {
      revalidate: 3600,
      tags: [`article-${slug}`, 'articles'],
    },
  });
  return res.json();
}

export default async function Page({ params }: { params: { slug: string } }) {
  const article = await getArticle(params.slug);
  return <article>{/* ... */}</article>;
}
// app/api/revalidate/route.ts, kutsu tätä CMS-webhookista
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const { slug, secret } = await req.json();

  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }

  revalidateTag(`article-${slug}`);
  return NextResponse.json({ revalidated: true });
}

Vercel-alustalla tämä toimii natiivisti globaalissa edge-cachessa. Itsehostatussa Next.js:ssä tarvitset @vercel/next-yhteensopivan runtimen tai Vercelin ISR-dokumentaation mukaisen mukautetun cache handlerin (Redis, Cloudflare KV, S3+CloudFront). Oma suositukseni itsehostatuille: Redis plus Cloudflare-etunaama. Silloin saat globaalin edge-cachen ilman Vercel-lukkiutumista.

bfcache ja Back/Forward-navigointi

bfcache on selaimen sisäinen välimuisti koko sivun tilalle (DOM, JS-heap, scroll-positio) navigoinnin yhteydessä. Kun käyttäjä painaa "takaisin", sivu palautuu välittömästi (LCP on käytännössä 0 ms). Chrome 96+ ja Safari 15+ tukevat sitä kaikilla laitteilla, ja Chrome UX Reportin 2026 mediaani back-cache-hit-rate on 60–70 %.

Tapoja rikkoa bfcache ilman että huomaa:

  • Cache-Control: no-store: Chrome kieltäytyy tallentamasta sivua bfcacheen.
  • unload-tapahtumakuuntelija: deprecoitu, käytä pagehide-eventtiä sen tilalla.
  • Avoin IndexedDB-transaktio navigoinnin hetkellä.
  • Cache-Control: no-cache yksin ei estä bfcachea, mutta yhdistelmä no-cache, no-store estää.

Auditoi Chrome DevToolsissa: Application → Back/forward cache → Test. Chrome kertoo tarkasti, mikä syy esti tallennuksen. Vinkki myös INP:n kannalta: bfcache-hit ei laukaise LCP-mittausta uudelleen, joten Speculation Rules API ja bfcache yhdessä muodostavat "zero-latency-navigation"-parin.

Mittaaminen: Cache-Status-otsikko ja hit-suhteen seuranta

RFC 9211 määrittelee Cache-Status-otsikon, jonka jokainen kerros lisää itseään kuvaavan merkinnän. Esimerkkivastaus, joka on kulkenut edge- ja regional-cachen läpi:

HTTP/2 200
cache-status: "CF-Edge"; hit
cache-status: "CF-Regional"; hit; ttl=2137
cache-control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
age: 1263

Kolme näistä on välttämättömiä lukemaan: hit/miss/fwd=stale, ttl (aikaa tuoreena) ja Age (kuinka kauan objekti on ollut cachessa). RUM-työkalu ei näe näitä oletuksena, joten pitää tallentaa PerformanceResourceTiming-metriikoiden yhteyteen. Jos käytät web-vitals-kirjastoa kenttädatan keräämiseen, laajenna beacon-payloadia mukaan Cache-Status-arvolla parseroituna hit-suhteen segmentointia varten.

Tavoitteet joita käytän tuotannossa:

  • HTML: edge-hit-suhde > 85 %, origin-liikenne < 5 %.
  • Staattiset assetit: edge-hit > 98 %, cold miss vain deploylla.
  • API: hit > 60 %, stale-served < 10 %.

Yleisimmät virheet ja miten välttää ne

Kentältä yleisimmät kolme, ja olen törmännyt jokaiseen henkilökohtaisesti tuotannossa:

  1. Cache-Control: no-cache ymmärretään "älä tallenna" -direktiiviksi. Oikeasti se tarkoittaa "tallenna mutta validoi ennen käyttöä". Jos haluat estää tallennuksen, käytä no-store.
  2. Cookielliset vastaukset merkitään public. Cloudflare oletuksena kunnioittaa cookieita eikä cachea, mutta Fastly ja mukautetut Varnish-konfiguraatiot voivat välttää tarkistuksen. Merkitse aina henkilökohtaiset vastaukset private.
  3. Sama URL palvelee eri sisältöä ilman Vary-otsikkoa. Klassinen: mobiili- ja desktop-versiot samasta osoitteesta. Tarvitset Vary: User-Agent (varo cache-fragmentaatiota), tai parempi ratkaisu on erilliset URLit.

Neljäs yleinen virhe on yrittää välimuistittaa personoituja sivuja. Oikea kaava on edge-side compose: cachettu shell yhdistettynä edge-koottuun personoituun fragmenttiin. Cloudflare Workers, Fastly Compute@Edge ja Vercel Edge Middleware tarjoavat tähän valmiit mallit. Jos yrität cachea kokonaista personoitua sivua, joko tuhlaat välimuistin tilaa (per-käyttäjä-objektit) tai vuodat toisen käyttäjän dataa (hit väärälle sessiolle). Kumpikaan ei ole hauska.

Usein kysytyt kysymykset

Mikä on ero max-age ja s-maxage välillä?

max-age koskee yksityisiä välimuisteja (yksittäisen selaimen cache). s-maxage koskee jaettuja välimuisteja (CDN, proxy) ja ohittaa max-age-arvon jaetussa cachessa. Yleisin kaava on max-age=0, s-maxage=3600: selain hakee joka kerta, CDN palvelee tunnin.

Mitä stale-while-revalidate tekee käytännössä?

Kun cachettu vastaus vanhenee, CDN palauttaa vanhentuneen version välittömästi (~15 ms TTFB) ja käynnistää revalidointipyynnön origiiniin taustalla. Käyttäjä ei odota, ja seuraava vierailija saa tuoreen version. Se on paras yksittäinen työkalu TTFB-optimoinnissa vuonna 2026.

Onko immutable-direktiivi turvallista käyttää?

Kyllä, mutta vain URL-osoitteille, jotka sisältävät sisältöhashin (esim. app.a1b2c3.js). Jos sisältö muuttuu, deploy generoi uuden hashin ja siten uuden URL:n. Älä käytä immutable:a hashaamattomille URLeille, muuten käyttäjät jäävät jumiin vanhaan versioon kunnes he tyhjentävät välimuistin manuaalisesti.

Miten CDN-välimuisti tyhjennetään turvallisesti?

Käytä tag- tai surrogate-key-pohjaista invalidointia URL-purgen sijaan. Liitä jokaiseen vastaukseen tagit, jotka kuvaavat sen riippuvuuksia (esim. article-42, author-mateo), ja invalidoi tageilla. Vältä globaaleja tageja kuten all, joka aiheuttaisi thundering herd -tilanteen originiisi.

Miksi Cache-Control ei näytä toimivan Chromessa?

Yleisin syy on DevToolsin "Disable cache" -asetus päällä. Toinen: Vary-otsikko sisältää dynaamisen kentän kuten Vary: * tai Cookie. Kolmas: vastauksessa on Set-Cookie-otsikko, joka Chromen mukaan merkitsee "personoitua". Auditoi DevTools → Network → oikeaklikkaus → "Show cache related headers".

Kuinka pitkän max-age-arvon saan asettaa?

Hashatuille asseteille yksi vuosi (max-age=31536000) on standardi ja täysin turvallista immutable-flagin kanssa. HTML:lle pidä max-age=0 selaimessa ja käytä s-maxage+SWR-yhdistelmää CDN-kerroksessa. Erittäin harvoin muuttuva sisältö kuten kuvat voi käyttää viikkoja tai kuukausia.

Mateo Silva
Tietoa Kirjoittajasta Mateo Silva

Full-stack performance lead bridging frontend perf with backend latency. Cache invalidation is his love language.