Har din cache også løjet for dig?
Forestil dig at du deployer en vigtig bugfix, trykker refresh, alt ser fint ud… og så skriver brugeren fem minutter efter: “Jeg har stadig fejlen”. Du kigger på koden, den er jo opdateret. Men deres browser viser stadig den gamle verden.
De fleste gange hvor jeg har haft den slags “spøgelsesbugs”, har det været cache-control der drillede. Ikke fordi HTTP caching er ondt, men fordi jeg ikke helt forstod hvem der gemte hvad, og hvor længe.
Hvem gemmer egentlig hvad, og hvornår?
Jeg starter lige med den mentale model, jeg selv ville ønske nogen havde tegnet for mig tidligere.
Tre lag der kan cache dine svar
Typisk har du mindst tre lag i spil:
- Browseren – gemmer svar lokalt hos brugeren
- CDN eller reverse proxy (Cloudflare, Vercel, Netlify, nginx osv.) – gemmer svar tæt på brugeren
- Din app/server – selve kilden der genererer svaret
Alle tre kan være inde i billedet på samme request. Og alle tre kigger på de samme HTTP headers: især Cache-Control, ETag, Last-Modified og nogle gange Vary.
To typer caching: gem eller spørg først
Der er groft set to strategier:
- Freshness: “Det her svar er gyldigt i X sekunder” (
max-age) - Revalidation: “Spørg mig lige, før du bruger det igen” (ETag / If-None-Match)
Freshness er hurtigst. Revalidation er sikrest. Det interessante er kombinationen af dem.
Cache-Control i praksis: de direktiver du faktisk bruger
Her er et eksempel på en Cache-Control header, jeg selv har brugt forkert mange gange:
Cache-Control: no-cache
Da jeg først så no-cache, troede jeg det betød “ingen caching”. Det gør det ikke. Det betyder “du må gerne gemme det, men du skal spørge mig, før du bruger det igen”.
max-age: hvor længe er svaret frisk?
max-age siger hvor mange sekunder svaret må bruges uden at spørge serveren igen.
// 1 time i browseren
Cache-Control: public, max-age=3600
public betyder at både browser og mellemled (CDN) må cache det. Uden public kan nogen proxies være mere forsigtige.
s-maxage: specielt til CDN og shared caches
s-maxage er som max-age, men kun til shared caches (typisk CDN).
// CDN må gemme i 1 dag, browser kun i 5 minutter
Cache-Control: public, max-age=300, s-maxage=86400
Det mønster er ret nyttigt til HTML-sider: brugeren får hurtige svar fra CDN, men browseren tør du ikke lade cache for længe, fordi du gerne vil kunne “tvinge” opdatering.
no-store vs no-cache: den klassiske misforståelse
no-store: intet må gemmes nogen steder. Hver request skal hele vejen til serveren. Bruges til ting som login-sider, personlige dashboards med følsomme data osv.
no-cache: må gerne gemmes, men skal revalidere før brug. Så svaret kan ligge på disk, men browseren sender en hurtig “må jeg bruge det her?” forespørgsel.
// Ingen caching overhovedet
Cache-Control: no-store
// Gem det, men spørg før du bruger det igen
Cache-Control: no-cache
Jeg har selv brugt no-cache når jeg mente “ingen cache”. Resultat: ekstra requests, forvirring og stadig mærkelig adfærd.
ETag og If-None-Match: sådan foregår revalidation
ETag er i praksis bare et stykke tekst, serveren sætter på svaret, så den kan genkende indholdet senere.
Hvordan ETag-flowet ser ud i virkeligheden
- Bruger henter
/style.css - Server svarer med:
HTTP/1.1 200 OK
ETag: "v1-abc123"
Cache-Control: max-age=0, must-revalidate
/* CSS-indhold */
- Næste gang browseren skal bruge
/style.css, sender den:
GET /style.css
If-None-Match: "v1-abc123"
- Hvis indholdet ikke har ændret sig, svarer serveren:
HTTP/1.1 304 Not Modified
// Ingen body, browser bruger sin cachede version
Fordelen: du sparer båndbredde, og browseren får sikkerhed for, at indholdet er det samme.
Svag vs stærk ETag (den korte version)
Du kan støde på noget som:
ETag: W/"123456"
W/ betyder “weak”. Browseren må stadig bruge svaret, men svage ETags kan betyde “næsten det samme”. Personligt bruger jeg kun ETag hvis mit framework sætter det for mig. Ellers versionerer jeg filer via filnavne (for CSS/JS) og bruger Last-Modified eller slet ingenting til HTML.
stale-while-revalidate: hurtig oplevelse uden at opgive friskhed
stale-while-revalidate er et relativt nyt direktiv, som mange CDNs og moderne browsere forstår. Det lyder kompliceret, men ideen er simpel: “Brug det gamle svar med det samme, men begynd i baggrunden at hente et nyt”.
Et eksempel på en header med stale-while-revalidate
Cache-Control: public, max-age=60, stale-while-revalidate=300
Det betyder:
- I de første 60 sekunder er svaret friskt (ingen revalidation)
- Efter 60 sekunder må cache stadig bruge det gamle svar i op til 300 sekunder, men samtidig trigge en baggrundsrevalidation
Det føles for brugeren som om siden altid er hurtig, og din origin-server får færre “kolde” hits.
Hvornår er stale-while-revalidate en god ide?
Jeg bruger det gerne til:
- Statiske sider der opdateres sjældent (blogindlæg, dokumentation)
- API-svar med data der ikke skal være sekund-præcist (statistikker, “seneste artikler” osv.)
Jeg undgår det til:
- Data hvor en forsinkelse giver rod (ordrestatus, saldo osv.)
- Personligt indhold hvor hver bruger skal se sit helt eget billede af verden
Tre opskrifter der dækker 90% af dine use cases
Nu til den del jeg selv manglede: hvad sætter man rent faktisk på hvilke svar? Det her er ikke den eneste sandhed, men det er et fornuftigt udgangspunkt.
1) Statiske assets (CSS, JS, billeder, fonde)
Her vil du gerne cache så hårdt som muligt, når først filen har et unikt navn. Det klassiske mønster:
/assets/app.abc123.css
/assets/app.abc123.js
/images/logo.7f9e2.png
Når du deployer, ændrer du hash-delen i filnavnet. Browser og CDN kan derfor cache for evigt, fordi en ny version altid har et nyt navn.
Headers kan se sådan her ud:
Cache-Control: public, max-age=31536000, immutable
immutable fortæller browseren at filen aldrig ændrer sig, så den behøver ikke engang at revalidere.
Typisk fejl jeg selv har lavet: bruge lang cache uden filnavn-versionering. Resultat: brugere ser gamle styles i dagevis, indtil deres cache naturligt udløber.
2) HTML-sider
HTML er tit der hvor “cachen lyver” føles værst. Du vil gerne have hastighed, men du har også brug for at kunne ændre indholdet uden at vente på 24 timers cache-expiry.
Et ofte fornuftigt kompromis hvis du har CDN foran:
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=600
Oversat:
- Browser: må bruge svaret i 60 sekunder
- CDN: må bruge svaret i 600 sekunder (10 minutter)
- CDN: må bruge det lidt for gamle svar i yderligere 600 sekunder, mens det henter friskt i baggrunden
Hvis du vil være endnu mere forsigtig (f.eks. for admin-sider), kan du sige:
Cache-Control: no-store
Og så lade serveren selv stå for eventuel intern caching.
3) API responses
Her afhænger det meget af data-type og klienter, men der er nogle mønstre.
API med sjældent ændrede data
Eksempel: liste over lande, valutaer, produktkategorier.
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
ETag: "countries-v1"
Det gør svarene hurtige og billige, og du kan roligt leve med at nogen ser en lidt gammel liste i få sekunder.
API med ofte ændrede eller bruger-specifikke data
Eksempel: “mine ordrer”, “min profil” eller realtids-dashboard.
Cache-Control: private, no-store
private betyder at svaret kun må caches i brugerens egen browser, ikke i shared caches. Kombineret med no-store betyder det i praksis: ingen caching.
Hvis du vil have ETag-revalidation men ingen browser-cache, kan du gøre noget i stil med:
Cache-Control: private, max-age=0, must-revalidate
ETag: "orders-xyz"
Browseren må gerne gemme en kopi, men skal altid spørge før den bruger den.
Sådan ser du hvad der faktisk sker i Chrome DevTools
Det tog mig pinligt lang tid at opdage, at DevTools i bund og grund fortæller dig alt om caching, hvis du kigger det rigtige sted.
Trin 1: Tænd for de rigtige kolonner
Åbn DevTools, gå til fanen Network, højreklik på kolonne-headeren og slå f.eks. disse kolonner til:
- Status
- Type
- Size
- Time
- Waterfall
Tryk F5 og kig på “Size”-kolonnen. Hvis der står (from disk cache) eller (from memory cache), ved du at browseren ikke snakkede med netværket.
Trin 2: Læs Response Headers
Klik på en række, gå til fanen Headers, og kig efter:
- Response Headers:
Cache-Control,ETag,Last-Modified - Request Headers:
If-None-Match,If-Modified-Since
Hvis du ser If-None-Match, er browseren i gang med revalidation. Kommer svaret tilbage som 304 Not Modified, har revalidation virket.
Trin 3: Brug “Disable cache” når du fejlsøger
I Network-fanen er der en lille checkbox: Disable cache. Den gælder kun mens DevTools er åben, men den er guld værd når du vil være sikker på, at et problem ikke “bare” er cache.
Mit typiske flow når jeg er i tvivl:
- Åbn DevTools
- Sæt flueben i Disable cache
- Hard reload (hold Shift nede og tryk reload-knappen)
- Tjek at responsen nu er frisk (ingen
(from disk cache))
Hvis fejlen forsvinder der, er caching stærkt mistænkt.
Tre cache-fejl jeg selv har lavet mere end én gang
Jeg kunne sikkert lave en længere liste, men de her tre går igen.
Fejl 1: Lang cache på HTML uden nogen form for versionering
Setup: statisk site på et CDN, jeg var glad for performance og satte:
Cache-Control: public, max-age=86400
Det virkede fint… indtil jeg skulle rette en skrivefejl på forsiden, og nogen stadig så den gamle side dagen efter. Jeg havde ingen måde at “invaliderer” deres cache på.
Bedre løsning: kortere max-age på HTML, længere på assets. Og så bruge “Purge cache” i CDN-konsolem når jeg absolut skal presse en opdatering igennem.
Fejl 2: API blev cachet bag CDN uden jeg lagde mærke til det
Jeg havde bygget et lille JSON-API til et sideprojekt og deployet det på en platform med indbygget CDN. Jeg satte aldrig nogen specifikke cache-headers. CDNen valgte alligevel at gemme svar for mig.
Resultat: en bruger opdaterede data, men deres GET-kald til APIet viste stadig gamle værdier i op til et minut. På min maskine virkede det, fordi jeg ofte bypassede CDN i udviklingsmiljøet.
Løsning: være eksplicit:
Cache-Control: private, no-store
på alle API-endpoints hvor hver enkelt request skal være frisk.
Fejl 3: Manglende Vary-header gjorde alt for aggressiv caching
Vary fortæller caches, at et svar afhænger af en given header. Et klassisk eksempel er sprog:
Vary: Accept-Language
Jeg havde en endpoint der returnerede forskelligt indhold afhængigt af en Authorization-header, men ingen Vary. CDN gemte bare første svar og brugte det til senere brugere.
Hvis du bygger noget lignende, så overvej:
Vary: Authorization
eller endnu bedre: cache ikke det svar i shared caches. Typisk med Cache-Control: private, no-store.
Tjekliste før du skruer caching op i produktion
Inden du går all-in på cache-control og aggressive værdier, kan du stille dig selv et par konkrete spørgsmål.
1. Hvad er det værste der sker, hvis nogen ser data der er X minutter gamle?
Hvis svaret er “de ser en gammel artikeloverskrift”, så er det nok fint med 5-10 minutters cache. Hvis svaret er “de får vist forkert saldo på deres konto”, så ved du godt hvad du skal svare.
2. Ved jeg hvordan jeg vil opdatere ting hurtigere end max-age?
Til HTML-sider på et CDN: har du en purge-knap du stoler på?
Til assets: bruger du hashes i filnavnene, så du kan lade gamle filer leve i cache i fred?
Hvis ikke, så skru lidt ned indtil du har en strategi.
3. Har jeg tjekket headers i DevTools, ikke bare gættet?
Tag et par kernesider og API-kald på dit site, åbn Network og kig helt konkret på:
Cache-ControlETag/If-None-Match- Status-koder: 200 vs 304
Gør det både med og uden “Disable cache”. Så kan du se forskellen, i stedet for at gætte på, hvad der sker.
4. Har jeg skelnet mellem HTML, assets og API i min konfiguration?
Det er fristende at sætte én global regel i fx nginx eller et CDN og så være færdig. Men HTML, statiske filer og API har ofte brug for helt forskellige regler.
Hvis du bruger et statisk site, der bygger på noget som Next.js eller Astro, så kig i deres dokumentation om anbefalede cache-headers. Der er ofte fornuftige defaults. Du kan f.eks. starte med at læse om caching på MDN’s side om HTTP caching og så mappe det til dit framework.
Hvor kan du gå videre herfra?
Hvis du har læst hertil, er du allerede foran de fleste, der bare skruer på cache-knappen indtil “det føles hurtigt”. Et naturligt næste skridt er at lege med et lille testprojekt, hvor du bevidst sætter forskellige Cache-Control-kombinationer og ser effekten i DevTools.
Du kan også kombinere det her med andre performance-greb du måske allerede kender, som at optimere assets, bruge moderne bundlere eller sætte et simpelt build-setup op, som jeg var inde på i en anden artikel om workflow. Og hvis du arbejder full stack og gerne vil forstå hele kæden fra browser til server, hænger caching godt sammen med forståelsen af API-design, som i artiklen om pagination og API-responser på codingclass.dk.
Jeg synes caching er et af de områder, hvor en lille investering i forståelse giver uforholdsmæssigt meget ro i maven senere. Spørgsmålet er mest: hvor aggressivt tør du cache, nu hvor du ved hvordan du slår det fra igen?









Send kommentar
Du skal være logget ind for at skrive en kommentar.