Bookmark and Share
Her er du: Forsiden » PHP-artikler » HTTP-caching
Indhold
Nyhedsbrev

Få en mail når vi skriver en ny PHP-artikel, holder et nyt gratis kursus, eller når der sker andet godt i vores verden:

Din email:
 

HTTP-caching

Med HTTP-caching kan dine sider vises drabeligt meget hurtigere end hvis du blot serverer siderne på klassisk vis - uheldigvis sker det ikke automatisk for PHP-sider, så her får du en gennemgang af hvordan det lykkes for dig.

Hvad er HTTP-caching og hvorfor er det godt?

Konceptet http-caching går i al sin enkelthed ud på, at hvis folk har hentet en ressource fra din server én gang, så behøver de ikke hente den igen næste gang de skal bruge den.

Det giver umiddelbart mening for grafik, CSS og måske JavaScript-filer der går igen på alle dine sider, og den slags statisk indhold klarer webserveren som oftest fint at få cachet i folks browsere, så de kun hentes en enkelt gang.

Værre ser det ud med dine dynamiske PHP-sider - de caches som udgangspunkt aldrig, og det er det vi vil gøre noget ved i denne artikel.

For det er ikke kun båndbredde der kan spares - hvis folk ser på cachede kopier af din side, så behøver du jo slet ikke fyre op for al din PHP-kode, snakke med databaser og andre dyre ting, og så begynder det pludselig at give bedre overordnet performance på din server.

Headers og HTTP returkoder

Inden vi rigtig går igang skal vi dog lige have styr på hvad HTTP-headers og -returkoder er for noget.

Begge dele er noget man som almindelig internet-bruger aldrig opdager. Headers og returkoder er er "skjult" kommunikation mellem din browser og den webserver du beder om at se en side på. Udenfor dit synsfelt sender din browser nemlig en stribe headers til webserveren, som sender nogle headers tilbage inden du - måske - får den ønskede side at se.

Alt dette foregår under hjelmen, og det er godt nok for de fleste, men hvis du programmerer dynamiske sider er der gevinster at hente ved at have styr på HTTP-headers.

Men lad os for eksempel se på hvad der sker i kulissen når du klikker dig ind på denne artikel.

Din browser sender nogle headers til vores server, som kunne se sådan ud:

GET /php/http-caching HTTP/1.1
Host: moski2.net
User-Agent: Mozilla/5.0 Gecko/20091221 Firefox/3.5.7
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

Kort fortalt betyder det, at du gerne vil se ressourcen /php/http-caching på serveren moski2.net - resten er i denne sammenhæng ligegyldigt.

Nu svarer serveren så med en status-kode samt flere andre headers:

HTTP/1.1 200 OK
Date: Sun, 28 Mar 2010 22:14:57 GMT
Server: Apache
X-Powered-By: Moski2.net
Etag: "8edc0574ab98fe80dc6b3291e38411ef"
Cache-Control: max-age=0, must-revalidate
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 5033
Content-Type: text/html; charset=utf-8

Returkode 200 betyder at ressourcen er fundet, og at den bliver sendt til dig lige efter disse headers - herefter viser din browser artiklen du læser ligenu.

Hele denne kommunikation er som sagt skjult for dig som almindelig internet-bruger, men som udvikler har du mulighed for at bestemme hvilke headers der skal sendes fra webserveren, og det er en del af metoden til at opnå god caching af dine PHP-sider - vi kommer især til at kigge på de to headers "Etag" og "Cache-Control".

Returkoder og caching

Hvis vi skal simplificere lidt og omsætte den ovenstående skulte kommunikation mellem browser og webserver der foregår når du hopper ind på siden her, så kunne det se sådan ud:

Almindelig HTTP-request uden caching - returnerer en statuskode 200

Når siden du beder om er fundet, kan webserveren i stedet for at sende en returkode 200, som i eksemplet her, også vælge at sende en returkode 304, hvilket betyder at du har allerede været her, og at siden har ikke ændret sig siden sidst.

Så vil din browser dykke ned i sin cache og vise dig en lokal kopi den gemte af siden sidst du var inde på den, og det er mange gange hurtigere end at skulle hente den ned fra webserveren igen:

Resultatet af en request efter en cachet ressource - returnerer en statuskode 304

Men hov, hvordan kunne webserveren vide at du havde været forbi tidligere?

Det kan den fordi den første gang sendte Etag-headeren med ud til dig. Etag-headeren er et unikt "stempel" på hvordan siden ser ud ligenu, og sålænge siden ikke ændrer sig skal serveren udlevere det samme Etag med siden hver gang.

Næste gang du så spørger efter den samme side på serveren vil din browser automatisk sende det Etag med tilbage som den fik udleveret første gang - når browseren sender Etag retur hedder headeren blot "If-None-Match" i stedet. Nu kan serveren så sammenligne med det nuværende Etag - er de to éns betyder det at siden ikke har ændret sig siden sidst browseren fik den udleveret, og serveren kan sende en statuskode 304 tilbage.

For at tvinge browseren til at checke om den lokalt cachede kopi af siden matcher den på serveren fik du også en Cache-Control-header med der sagde "must-revalidate", og det er kombinationen af Etag og Cache-Control der gør at browseren opfører sig som vi gerne vil have.

Bemærk også at headeren "Content-Length" er nul, hvilket betyder at browseren ikke skal forvente at få noget indhold udleveret. Det stemmer jo meget godt med at du i stedet skal se din lokale kopi.

Sådan får du cachet din PHP-side

Som nævnt håndterer din webserver sikkert hele balladen med Etag og If-None-Match headers automatisk når det gælder statisk indhold, men når din side genereres af PHP ser det anderledes ud - der er det nemlig dig selv der skal smøge ærmerne op.

For tager jeg for eksempel og bygger en helt simpel PHP-side:

<?php

echo 'Hej med dig';

?>

Så sender din webserver ikke nogen Etag-header med tilbage fordi den tænker "hmm, en PHP-side er dynamisk, så folk bør nok ikke cache den".

Altså må du selv tage fat, og det betyder at du skal lave en streng der kan fungere som Etag, og så skal du sende en afsted i en header. Vi laver Etag-værdien som en checksum af filens "modification time" - altså tidspunktet for hvornår filen sidst blev rettet. På den måde vil Etag-værdien ændre sig hvis du ændrer i dit script:

<?php

$etag = '"' . md5( filemtime(__FILE__) ) . '"';

header('HTTP/1.1 200 OK');
header('Cache-Control: public, must-revalidate');
header('Etag: ' . $etag);

echo 'Hej med dig';

?>

Bemærk: Etag-værdien er omsluttet af dobbelt anførselstegn modsat alle andre headers.

Nu ryger der en Etag-header med ud til browseren, men vi er stadig ikke i mål, for selv om browseren sender Etag-værdien med som en If-None-Match header næste gang, så gør dit script jo nøjagtigt det samme.

Altså skal vi lige inspicere om der kommer en If-None-Match header med, og gør der det kan vi sende en 304 returkode tilbage. I PHP finder du headeren If-None-Match i variablen $_SERVER['HTTP_IF_NONE_MATCH']:

<?php

$etag = '"' . md5( filemtime(__FILE__) ) . '"';

if( isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag == $_SERVER['HTTP_IF_NONE_MATCH'] ) {
  header('HTTP/1.1 304 Not Modified');
  header('Content-Length: 0');
  exit;
}

header('HTTP/1.1 200 OK');
header('Cache-Control: public, must-revalidate');
header('Etag: ' . $etag);
echo 'Hej med dig';

?>

Nu vil PHP aldrig komme så langt som til at skrive "Hej med dig" - vi stopper simpelthen når vi har fortalt browseren at den kan vise det den har liggende i cachen.

I dette eksempel er gevinsten ikke så stor, da vi bare skriver en tekst ud, men forestil dig at du gravede dybt i en database eller andre eksterne systemer og jongerede rundt med en masse data inden de skulle præsenteres, så var der pludselig noget at hente - både for din server, der ikke blev så belastet, og for din gæst, som ville få en markant hurtigere load-tid.

Tænk godt over dit Etag

Hvis du arbejder med dynamisk indhold nytter det naturligvis ikke noget at dit Etag udelukkende baserer sig på scriptets modification time, som vist ovenfor - en dynamisk side kan jo netop generere forskelligt indhold udfra en stribe parametre uden at koden ændrer sig.

Hvis dit script for eksempel hiver en nyhed ud af en database baseret på et ID, så kan du jo føje ID'et til den streng du laver en checksum af, og viser du en liste af nyheder, så må du lave en checksum på hele listen så Etag'et ændrer sig hvis listen ændrer sig.

Sidder du og river dig i håret over at dine sider forbliver cachede selv om indholdet faktisk har ændret sig i databasen, så har du givetvist misset et eller andet i grundlaget for dit Etag.

Hvis du bruger sessions

Der er dog en anden, og sikkert mere sandsynlig, årsag til at du kan ende med at rive endog store hårtotter af når du arbejder med PHP og http-caching, og det er hvis du bruger sessions på din side.

Hvis vi udvider vores eksempel fra før til først at fyre op for sessions:

<?php

session_start();

$etag = '"' . md5( filemtime(__FILE__) ) . '"';

if( isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag == $_SERVER['HTTP_IF_NONE_MATCH'] ) {
  header('HTTP/1.1 304 Not Modified');
  header('Content-Length: 0');
  exit;
}

header('Cache-Control: public, must-revalidate');
header('HTTP/1.1 200 OK');
header('Etag: ' . $etag);
echo 'Hej med dig';

?>

Så ser du til din rædsel følgende headers når du tilgår siden:

Date: Sun, 28 Mar 2010 23:07:40 GMT 
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Etag: "f5c9d40e9bc4adc1eafe34af9c93d0d8"
Content-Length: 11
Content-Type: text/html; charset=utf-8

Din Etag-header er der godt nok, men Cache-Control er ikke den du har lavet - i stedet er der en Cache-Control-header som gør hvad den kan for at siden ikke kan caches.

Og oveni hatten er der sat en Expires-header som fortæller at siden er håbløst forældet, hvilket vil få de fleste browsere til at hente en frisk version fra serveren.

Problemet er at PHP sætter flere headers når sessions er i spil for at undgå caching. Du kan selv i nogen grad bestemme hvilke der skal sættes ved at bruge funktionen session_cache_limiter eller ved at sætte session.cache_limiter-værdien via ini_set-funktionen.

PHP tilbyder fire præ-definerede værdier for session.cache_limiter, som hver især afstedkommer nogle faste headers. Desværre er disse alle ret ubrugelige i denne sammenhæng, men heldigvis er der den udokumenterede feature, at sætter man værdien til noget andet end de fire præ-definerede værdier, så bliver der ikke sat nogle headers overhovedet, og så træder dine egne headers i kraft.

Altså kan vi for eksempel sætte værdien til "dummy":

<?php

ini_set('session.cache_limiter', 'dummy');

session_start();

$etag = '"' . md5( filemtime(__FILE__) ) . '"';

if( isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag == $_SERVER['HTTP_IF_NONE_MATCH'] ) {
  header('HTTP/1.1 304 Not Modified');
  header('Content-Length: 0');
  exit;
}

header('Cache-Control: public, must-revalidate');
header('HTTP/1.1 200 OK');
header('Etag: ' . $etag);
echo 'Hej med dig';

?>

Og straks ser dine headers bedre ud:

Date: Sun, 28 Mar 2010 23:25:12 GMT 
Server: Apache
Cache-Control: max-age=0, must-revalidate
Etag: "d2e163986af19c0c073cafe7c825881c"
Content-Length: 11
Content-Type: text/html; charset=utf-8

Moski2 CMS klarer den for dig

Som du kan se af artiklen her så kan der være meget at holde styr på hvis dine sider skal caches korrekt i folks browsere, men klarer du den, så er der en kæmpe fordel for gengangere på dit website.

Du har selvfølgelig også den mulighed at du baserer dit site på Moski2 CMS - så får du automatisk HTTP-caching serveret på et sølvfad uden at du skal løfte en finger. Moski2 CMS sørger for at indhold bliver cachet lige indtil du ændrer i indhold eller templates, og du kan koncentrere dig om vigtigere ting, mens du med pralende sindro kan forsikre din kunde om at der bliver cachet på livet løs.

Alt det vi ikke fik sagt

HTTP-caching er et omfattende emne, og her har fokus blot været på at få cachet dynamiske PHP-sider på en god måde. Der findes andre teknikker som kan være mere relevante i andre sammenhænge - hvis du har lyst til at læse mere, så er der en rigtig god gennemgang af HTTP-caching ovre hos Mark Nottingham som banker endnu flere koncepter på plads.

Det skal også nævnes at metoden vi har brugt i artiklen her udelukkende fungerer i HTTP 1.1 og ikke i HTTP 1.0. Alle moderne browsere arbejder dog i HTTP 1.1, og kommer der en HTTP 1.0-klient forbi (som mange søgerobotter er) er det jo ikke værre end at klienten ser en frisk version af din side og ikke en cachet.

Kan du slet ikke få nok kan du jo læse lidt op på HTTP-headers og returkoder:

God caching.