Cachen lyver oftere end din kode

Cachen lyver oftere end din kode

Jeg har engang brugt en hel aften på at jagte en “mystisk bug” i noget JavaScript, for til sidst at opdage, at det eneste mystiske var, at min browser nægtede at hente den nye fil. Koden var fin. Min caching-strategi var det ikke.

Hvis du også har prøvet at deploye, trykke refresh og stadig se gammel opførsel, så er det ikke dig, der er skør. Det er HTTP caching, der gør præcis det, du har bedt den om. Bare lidt for godt.

1. Hvad caching egentlig gør i én sætning

Hvis jeg skal koge det helt ind: HTTP caching handler om, hvem der må genbruge et svar, hvor længe og på hvilke betingelser.

Der er tre “steder”, der typisk gemmer dine svar:

  • Browseren – cache per bruger, per device.
  • CDN / reverse proxy (Cloudflare, Vercel, Netlify, etc.) – cache tæt på brugeren, fælles for mange brugere.
  • Din egen server – oftest ikke cache, men den sender reglerne i form af HTTP headers.

Alle tre kigger på de samme ting: især Cache-Control, ETag, Last-Modified og et par stykker mere.

Så i stedet for at huske “magiske” browser-genveje, er det langt rarere at vide, hvorfor din main.js ikke bliver opdateret, og hvad du kan gøre ved det.

2. De 6 headers du faktisk støder på

Cache-Control

Cache-Control er din hovedfjende og bedste ven i samme header. Den fortæller, hvor længe noget må ligge i cache, og hvem der må gemme det.

Cache-Control: public, max-age=31536000, immutable

Her siger du: alle caches (browser + CDN) må gemme svaret i op til 1 år, og filen ændrer sig ikke (immutable). Brug det til fingerprintede statiske filer, ikke til HTML.

Typiske direktiver:

  • max-age=sekunder – hvor længe svaret er “friskt”.
  • no-cache – må gerne gemmes, men skal revalidere hos serveren før brug.
  • no-store – må slet ikke gemmes, hverken i browser eller CDN.
  • private – kun browseren må gemme, ikke shared caches (CDN).
  • public – må gemmes alle steder.

ETag

ETag er en slags fingeraftryk for indholdet. Hvis indholdet ændrer sig, ændrer ETag-værdien sig.

ETag: "abc123"

Når browseren har en cachet version, sender den den værdi tilbage næste gang:

If-None-Match: "abc123"

Hvis serveren stadig har samme version, svarer den med 304 Not Modified og ingen body. Browseren bruger så sin egen cachede body. Hurtigt, og du slipper for at sende det hele igen.

Last-Modified

Last-Modified er den lidt mere simple fætter til ETag. Den siger bare, hvornår ressourcen sidst blev ændret:

Last-Modified: Tue, 21 Jan 2025 10:15:00 GMT

Browseren kan så spørge:

If-Modified-Since: Tue, 21 Jan 2025 10:15:00 GMT

Og igen: hvis intet er ændret, svarer serveren med 304.

Expires

Expires er den gamle måde at sige “gyldig indtil”.

Expires: Wed, 22 Jan 2025 10:15:00 GMT

De fleste moderne setups bruger Cache-Control: max-age i stedet, men du ser tit begge to for bagudkompatibilitet.

Vary

Vary fortæller cache-laget, at svaret afhænger af en bestemt request-header.

Vary: Accept-Encoding

Det betyder: behold forskellige versioner afhængigt af fx om klienten understøtter gzip, br eller noget andet. På moderne sites ser du også:

Vary: Accept-Encoding, Accept-Language

Her kan samme URL have forskelligt indhold ud fra sprog.

Age

Age er en header, du typisk bare læser, når du debugger.

Age: 123

Det betyder, at svaret har ligget i en cache i 123 sekunder. Nyttigt, når du vil se, om du faktisk rammer CDN-cachen, eller hver request går direkte til backend.

3. no-cache, no-store og must-revalidate med rigtige eksempler

De tre ord lyder næsten ens, men de opfører sig meget forskelligt.

no-store

Cache-Control: no-store betyder: må aldrig gemmes nogen steder.

Brug det til ting som:

  • Kontoudtog
  • Personlige data
  • Svar med følsomme oplysninger
Cache-Control: no-store

Her bliver hver request kørt igennem hele vejen til serveren, hver gang.

no-cache

Cache-Control: no-cache er misvisende navngivet. Det betyder ikke “ingen cache”. Det betyder:

Må gerne gemmes, men skal altid tjekkes hos serveren, før det bruges.

Cache-Control: no-cache
ETag: "v1"

Browseren kan godt gemme svaret, men når brugeren beder om URL’en igen, sender browseren If-None-Match. Hvis server svarer 304, bruger browseren sin lokale cache. Du får både kontrol og hastighed.

must-revalidate

must-revalidate er en ekstra sikkerhedsline. Den siger: når max-age er udløbet, må cachen ikke bruge et gammelt svar uden at spørge serveren først.

Det er især interessant ved ustabile netværk, hvor en proxy ellers kunne finde på at servere “stale” (for gammel) cache.

Et mønster, jeg ofte bruger til HTML-sider:

Cache-Control: no-cache, must-revalidate

Så kan browsers og CDNs gemme, men der revalideres altid, når brugeren genindlæser.

4. 304 Not Modified: godt tegn, indtil det ikke er det

304 Not Modified betyder: “Du har allerede den nyeste version, brug bare den”.

Flowet ser typisk sådan ud:

  1. Første gang: browseren får 200 OK med body + ETag.
  2. Næste gang: browseren sender If-None-Match.
  3. Serveren tjekker: er ETag uændret?
  4. Hvis ja: svar med 304 og næsten ingen data.

Det er godt, fordi:

  • Du sparer båndbredde.
  • Du bevarer kontrol over, hvornår noget er forældet.

Det bliver et problem, når:

  • Du deployer ny kode, men din server af en eller anden grund genbruger samme ETag.
  • Du tror, at noget ikke er cachet, men du reelt bare får 304 hver gang.

Når du får en 304, er det ikke serverens skyld, at du ser gammel adfærd. Den siger jo bare “ingenting er ændret”. Så hvis du lige har deployet, og du stadig ser 304 på din main.js, er problemet typisk:

  • Manglende eller forkert cache busting (ingen fingerprint i filnavn).
  • Build-systemet genbruger gamle filnavne.
  • CDN, der ikke er blevet invalideret korrekt.

5. Static assets: fingerprint og lang cache

For statiske filer som JS, CSS og billeder findes der et mønster, der næsten altid er det rigtige: fingerprint + lang cache.

Idéen

I stedet for at hedde main.js hver gang, hedder din fil noget ala:

main.4f7a1c3.js

Det her hash (fingerprintet) ændrer sig, når indholdet ændrer sig. Så du kan gøre to ting:

  • Sætte lang cache på filen (f.eks. 1 år).
  • Sørge for, at din HTML peger på det nye filnavn efter hvert build.

Eksempel på headers til sådan en fil:

Cache-Control: public, max-age=31536000, immutable

immutable siger til browseren: “den her fil ændrer sig aldrig, så længe den har det her navn”. Og det passer, fordi navnet kun ændrer sig, når du bygger nyt.

Hvorfor dine ændringer ikke slår igennem uden fingerprint

Hvis du bliver ved med at bruge main.js uden hash, sker der typisk det her:

  • Du sætter fx Cache-Control: max-age=3600 (1 time).
  • Du deployer ny kode efter 5 minutter.
  • Brugere, der har hentet main.js lige efter deploy, sidder nu med en cachet version, som browseren er helt tilfreds med at bruge i op til en time.

Resultat: du ser nogle brugere på gammel adfærd, andre på ny. Det er her, mange får lyst til at spamme F12 og hard refresh, men det løser kun problemet for dig, ikke for alle andre.

De fleste moderne bundlere (Vite, Webpack, Parcel osv.) understøtter fingerprint out-of-the-box. Så tjek, at din production build faktisk laver filenames med hash, og at din HTML-peger på dem.

Hvis du vil læse mere om hvordan det spiller sammen med performance, har jeg også skrevet om simple performance-gevinster i Core Web Vitals uden panik.

6. API-responses: cache nogle ting, men ikke alt

For API-kald er caching lidt mere følsomt. Du vil typisk have tre slags data:

  • Næsten statiske (f.eks. en liste over lande, konstanter).
  • Hyppigt læste, sjældent opdaterede (f.eks. produktdata).
  • Personlige eller meget dynamiske (f.eks. brugerens nuværende kurv, profil, notifikationer).

Sådan kunne du tænke det

Eksempler:

Statiske opslagsdata:

Cache-Control: public, max-age=86400
ETag: "countries-v3"

Her kan både CDN og browser gemme svaret i 1 dag. Når du opdaterer listen, ændrer du ETag-versionen.

Produktdata til et site med ikke alt for store trafiktal:

Cache-Control: public, max-age=60, stale-while-revalidate=300

Her siger du: svaret er friskt i 60 sekunder. I op til 5 minutter efter kan cachen godt servere en lidt gammel version, mens den i baggrunden henter en frisk. Det hjælper især med performance, men du skal være ok med, at data kan være et par minutter bagud.

Bruger-specifikke data (fx kurv):

Cache-Control: private, no-cache
ETag: "cart-123"

Kun browseren må cache, og der skal revalideres ved refresh. Ingen CDN må gemme dine brugeres kurvdata.

Hvis du bygger API’er, der skal være til at leve med på længere sigt, giver det også mening at tænke cache ind sammen med de andre designvalg. Der er en artikel om hvordan du undgår det kaos-API, jeg selv startede med, som også er værd at have i baghovedet.

7. Debugging i Chrome DevTools: sådan ser du, om du rammer cache

Hvis du kun skal tage én vane med herfra, så er det den her: åbn Network-tabben og kig på, hvad der faktisk sker.

Trin-for-trin i Chrome

  1. Åbn siden i Chrome.
  2. Højreklik, vælg “Inspicer” (eller Cmd+Opt+I / Ctrl+Shift+I).
  3. Gå til fanen Network.
  4. Sæt evt. flueben i “Disable cache”, hvis du vil se rene requests (kun gældende mens DevTools er åbent).

Nu kan du se alle requests. Klik på f.eks. din main.js. Kig på:

  • Status: 200, 304, (from disk cache), (from memory cache).
  • Headers: hvad svarer serveren med af Cache-Control, ETag, etc.

Hvis du ser “from disk cache” eller “from memory cache”

Så betyder det, at browseren slet ikke har sendt noget til serveren. Den har bare sagt “jeg har det her liggende, det er stadig friskt, jeg bruger det bare”. Hvis du lige har deployet, og du stadig får det, er problemet typisk, at din max-age er for høj, og at du ikke bruger fingerprinted filnavne.

Hvis du ser 304 Not Modified

Så bliver serveren faktisk spurgt. Problemet, hvis der stadig er gammel kode, kan være:

  • Serveren genbruger samme ETag, selvom koden er ændret.
  • Et CDN har cachet et gammelt svar, og backend ser aldrig revalidations-forespørgslen.

En god lille øvelse er at prøve at skifte mellem “Disable cache” til og fra og se, hvad der ændrer sig. Så får du et mental billede af, hvor hårdt cachen slår til.

Hvis du ikke er vant til Network-tabben, kan du også læse min tidligere artikel om hvordan Chrome DevTools Network pludselig gav mening.

8. En simpel caching-opskrift til små webapps

Hvis du bare vil have noget, der virker for en typisk SPA eller lille webapp på Vercel/Netlify/Render, så er her et setup, jeg ofte ender med.

HTML (entry point, f.eks. index.html)

  • Status: må gerne gemmes kortvarigt, men skal være nem at opdatere.
Cache-Control: no-cache, must-revalidate

Så kan browseren godt gemme, men hver gang brugeren loader siden, bliver der tjekket, om der er en nyere version.

Fingerprintede JS/CSS-filer

  • Status: må gerne caches hårdt.
Cache-Control: public, max-age=31536000, immutable

Husk: det kræver, at filnavnene ændrer sig ved nyt build. Det klarer de fleste bundlere, hvis du bygger i “production mode”.

Billeder og fonte

Hvis de også er fingerprintede (f.eks. logo.abc123.png):

Cache-Control: public, max-age=31536000, immutable

Hvis du har “manuelle” billeder, du skifter ind imellem (f.eks. hero.jpg, uden hash):

Cache-Control: public, max-age=3600

Eller lav selv et simpelt cache-bust i URL’en, f.eks. hero.jpg?v=2, når du opdaterer billedet.

API-responses

Forslag til startsetup:

  • GET /api/public-data (ret statisk): Cache-Control: public, max-age=300
  • GET /api/user (privat): Cache-Control: private, no-cache
  • POST/PUT/DELETE: Cache-Control: no-store

Det er ikke perfekt til alle systemer, men det er et fornuftigt udgangspunkt, du kan justere fra.

Mini-øvelse

Hvis du vil teste din forståelse, så prøv:

  1. Åbn et eksisterende projekt, du har liggende online.
  2. Gå i Network, filtrer på “JS”.
  3. Noter for hver fil: har den hash i navnet? Hvad siger Cache-Control?
  4. Vurdér: svarer det nogenlunde til mønstret ovenfor, eller er alt bare “standard” uden omtanke?

Det giver et ret ærligt billede af, hvordan din nuværende deploy-strategi egentlig opfører sig.

9. Hvor går du videre herfra?

Det næste, jeg selv ville gøre, er at koble caching sammen med resten af performance-billedet. F.eks. hvordan lange caches på statiske filer spiller sammen med bundle-størrelse, lazy loading og den slags. Der er allerede lidt i artiklen om Core Web Vitals, men jeg har en mistanke om, at caching får sin helt egen lille nørdede serie på et tidspunkt.

Spørgsmålet er egentlig bare: hvor meget vil du kunne stole på, at dit næste deploy rent faktisk rammer brugerne, uden at du først skal forklare dem, hvordan de laver en hard refresh?

Åbn DevTools Network og genindlæs med cache deaktiveret for at se forskellen. Tjek response-headers med curl -I eller i DevTools: kig efter Age, CF-Cache-Status eller X-Cache for CDN-status og ETag/Last-Modified for revalidation. Hvis du får 304 Not Modified, var cachet indhold genbrugt; et 200 med en Age-header peger ofte på CDN.
Fingerprint dine statiske filer (fx main.abc123.js) og sæt lange max-age + immutable for disse filer. Hold HTML kort-levetid eller brug no-cache/no-store så browseren revaliderer ofte, og brug CDN-purge eller versionerede filnavne ved hasteændringer. Det kombinerer sikker levering med enkel invalidation.
ETag er mere præcis og anbefales når indhold kan ændre sig uden at modificeringstidspunktet ændrer sig, mens Last-Modified er enklere og billigere at beregne. Du kan godt sende begge; hvis du har fingerprintede assets er det dog ofte bedre at fokusere på filnavne og Cache-Control, da CDN'er nogle gange fjerner ETag.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed