Kešování HTML výstupu
- David Grudl
- Nette Core | 8218
Aneb jak ušetřit trafic serveru, uživatelům a snížit vytížení serveru.
Cache na straně klienta
Nejprve si nastudujte výborný Kešovací návod pro autory webu a webmastery.
Jak vidíte, kešování na straně klienta je závislé především na HTTP
hlavičkách Cache-Control
, Expires
,
Last-Modified
a ETag
. Nette má zabudovanou podporu
pro tyto hlavičky. Stačí zavolat metodu
$presenter->lastModified($modified, [$etag, [, $expire]])
, kde
$modified
nese čas poslední změny stránky (jako UNIX timestamp)
a $etag
libovolný validační řetězec. Metoda jednak nastaví
hlavičky Last-Modified
nebo ETag
, ale především
zkontroluje, zda se klient neptá hlavičkou If-Modified-Since
,
jestli se stránka od určitého času nezměnila, nebo nekontroluje hlavičkou
If-None-Match
hodnotu validačního identifikátoru. Pokud se
nezměnila, je presenter ukončen metodou terminate()
. Ušetří se
čas generování stránky a přenesený objem dat na straně serveru
i klienta.
Volitelný třetí parametr $expire
nastaví HTTP hlavičky
Cache-Control a Expires (hodnotou expire
je buď UNIX timestamp,
nebo počet sekund od „včil“, nebo FALSE, což znamená: nekešuj!).
Metoda lastModified pracuje jen na produkčním serveru, tedy když
Environment::isProduction() vrátí FALSE
. To lze ovlivnit přes
Environment::setMode('production', TRUE)
.
Cache na straně serveru
Na straně serveru máme daleko víc možností, co a jak kešovat. Můžeme kešovat výstup jednotlivých komponent, dílčí procesy, nebo celé HTML stránky. Jako doporučenou literaturu bych uvedl zdejší návod na Nette\Caching.
Jak kešovat třeba HTML výstup z celého presenteru? Přepíšeme si
například metodu startup()
presenteru, aby zjistila, jestli
stránka už není v cache a pokud ne, zapne output buffering:
private $cacheKey;
/**
* @return void
*/
protected function startup()
{
// vytvoříme cache ve jmenném prostoru 'application/output'
// (jméno prostoru je libovolný řetezec)
$cache = Environment::getCache('application/output');
// klíčem bude třeba jméno presenteru a view + obsah parametru id
$key = $this->getName() . ':' . $this->getAction() . '#' . $this->params['id'];
// ověření, zda je položka v keši
if (isset($cache[$key])) {
echo $cache[$key]; // vypsat a finíto
$this->terminate();
} else {
ob_start();
$this->cacheKey = $key;
}
}
/**
* @param Exception optional catched exception
* @return void
*/
protected function shutdown($exception)
{
if ($this->cacheKey) {
$content = ob_get_clean();
if ($content) {
echo $content;
$cache = Environment::getCache('application/output');
$cache->save($this->cacheKey, $content, array(
'expire' => 300, // 300 s = 5 minut
));
}
}
}
Při ukládání do keše metodou save() můžeme nastavit další parametry, viz zmíněný návod Jak funguje kešování v Nette. Šikovné jsou zejména tagy. Například stránky blogu můžeme do cache uložit s dlouhou (cca nekonečnou) expirací a otagovat je tagem např. ‚item/123‘, kde 123 je ID článku. Když pak někdo napíše na blog komentář, nebo když uživatel zedituje článek, tak se invaliduje příslušný tag ‚item/xyz‘ a stránka se smaže s cache.
- veena
- Člen | 98
Furt se to snažím pochopit a moc mi to nejde.
Kešováním pomocí:
$presenter->lastModified($modified[, $etag, [, $expire]])
bych chtěl dosáhnout toho, aby když uživatel přijde na stránku, tak se pro něho zakešovala na xx sekund, asi že?
Tzn expires bych nastavil na těch xx sekund, ale na co mám nastavit $modified? Na now?
Neni mi totiž jasná tahle část:
Metoda jednak nastaví hlavičku
Last-Modified
, ale především zkontroluje, zda se klient neptá hlavičkouIf-Modified-Since
, jestli se stránka od určitého času nezměnila … Pokud se nezměnila, je presenter ukončen metodouterminate()
. Ušetří se čas generování stránky a přenesený objem dat na straně serveru i klienta.
Konkrétně se píše „jestli se stránka od určitého času nezměnila“ Od jakého času? Od toho co byl prohlížeči nastaven v hlavičce last modified?
Tu cache na straně serveru bych použil při nakechování celé stránky třeba po pěti minutách pro všechny uživatele, to chápu.
Editoval veena (14. 8. 2008 9:34)
- phx
- Člen | 651
Vtip je v tom, ze prohlizec aby nemusel tahat celou stranku posle info „mam doma v cache stranku z …“ A server but rekne ta je OK a nice se neprenasi. A nebo nee tady mas novou.
Aby toto fungovalo musi to podporovat jak prolizec tak i webapp.
Na uzivateli je tedy aby spravne rekl metode
$presenter->lastModified()
kdy byla stranka naposled zmenena.
Dle toho se vyhodnoti co jsem jiz psal.
- Panda
- Člen | 569
Jinými slovy: $modified
by se měl nastavit na čas modifikace
nejnovějšího prvku na stránce.
Pokud mám tedy na stránce třeba prvky: článek, blok s novinkami a anketu, článek byl naposledy modifikován včera, do bloku s novinkami přišla poslední zpráva před půl hodinou a počet hlasů v anketě se změnil před minutou, tak to nastavím na čas modifikace (tzn. posledního hlasu) té ankety. Pokud bych pak upravil článek, ale nevytvořil bych žádnou novinku a nikdo by mi nezahlasoval do ankety, tak to musím nastavit na datum modifikace článku.
Konkrétní řešení by vypadalo třeba definicí nového rozhraní (a
k tomu rovnou třeba i abstraktní komponentu, která by část práce
udělala za programátora), které by nakazovalo implementovat metodu třeba
getLastModificationTime
. Ta by vracela čas poslední modifikace.
Všechny prvky na stránce, které by se mohly změnit, by implementovaly toto
rozhraní/dědily od komponenty. Ve vhodný okamžik (tzn. v moment, kdy už
všechny komponenty budou znát datum své poslední modifikce – záleží na
konkrétní aplikaci, pravděpodobně by to však mělo být před
renderováním, nebo na konci metody render{view}()
) by se prošly
a našel by se čas poslední modifikace. Pokud by stránka s nějakou
komponentou raději neměla být kešována, tak stačí, aby její
LastModified()
vždy vracelo now
.
- David Grudl
- Nette Core | 8218
V revizi č. 55 jsem implementoval podporu pro ETag, jakožto silný validační identifikátor. (čas poslední změny se obvykle považuje za slabý identifikátor, protože není schopen podchytit změny v rámci 1 sekundy). ETag je vlastně libovolný řetězec, obvykle se generuje jako MD5() nějakého klíče.
BC break: změnilo se pořadí parameterů metody lastModified(), úvodní post jsem aktualizoval.
- veena
- Člen | 98
Panda napsal(a):
Pokud by stránka s nějakou komponentou raději neměla být kešována, tak stačí, aby její
LastModified()
vždy vracelonow
.
Co se stane, když sem nastavil LastModified(time()-120)
?
Zakešuje mi to po prvním přístupu na 2 minuty v prohlížeči?
Mě to mozek prostě nějak furt nebere :-(
Editoval veena (21. 8. 2008 23:18)
- David Grudl
- Nette Core | 8218
Hele, četl jsi pozorně ten Kešovací návod pro autory webu a webmastery? Ať tu neřešíme něco, co je lépe popsané jinde.
- romansklenar
- Člen | 655
David Grudl napsal(a):
Metoda lastModified pracuje jen na produkčním serveru, tedy když
Environment::isProduction() vrátí FALSE
. To lze ovlivnit přesEnvironment::setMode('production', TRUE)
.
Sice to je takhle napsáno i v dokumentaci, ale není to překlep?
Já měl za to, že na produkčním serveru by mělo
Environment::isProduction()
být TRUE
FALSE by vrátilo jen na localhostu, pokud nastavím prostředí PRODUCTION a dojde na detekci módu
Environment::setName(Environment::PRODUCTION);
Environment::isProduction(); // FALSE
protože mód production se určuje podle adresy serveru.
Proto radši používám takovéto nastavení v bootstrapu
// Environment::setName(Environment::DEVELOPMENT);
Environment::setName(Environment::PRODUCTION);
$config = Environment::loadConfig();
Environment::setMode('production', Environment::getName() == Environment::PRODUCTION ? TRUE : FALSE);
Environment::setMode('debug', Environment::getName() == Environment::DEVELOPMENT ? TRUE : FALSE);
Debug::enable(NULL, NULL, $emailHeaders);
// parametr logování chyb je NULL, takže se rozhodne podle autodetekce z Environment podle řežimu production, tzn: Environment::isProduction() ? 'logovat' : 'zobrazovat'
if (Environment::getName() == Environment::DEVELOPMENT) { Debug::enableProfiler(); }
// a možná se bude hodit i automatické zachytávání výjimek
$application = Environment::getApplication();
$application->catchExceptions = Environment::getName() == Environment::PRODUCTION ? TRUE : FALSE;
// nebo klidně takto, protože Environment::isProduction() již vrací TRUE
$application->catchExceptions = Environment::isProduction() ? TRUE : FALSE;
kde výběr prostředí a tím i chování aplikace
(zachytávání/zobrazování výjimek a logování) provádím jen
zakomentováním jednoho ze dvou řádků pro nastavení jména prostředí,
vše ostatní se už pak odvíjí od toho, takže pokud teď chci na localhostu
nasimulovat chování v produkčním prostředí, správně se mi nastvaví
i mód production
.
Pokud to uznáte za přínosné, může se to doplnit jako tip do dokumentace.
- David Grudl
- Nette Core | 8218
romansklenar napsal(a):
Sice to je takhle napsáno i v dokumentaci, ale není to překlep?
jasně, je to překlep.
Proto radši používám takovéto nastavení v
bootstrapu
Environment::setMode('production', Environment::getName() == Environment::PRODUCTION ? TRUE : FALSE); Environment::setMode('debug', Environment::getName() == Environment::DEVELOPMENT ? TRUE : FALSE);
Můžeš si pro každé prostředí dát do config.ini
[production]
mode.production = yes
[development]
mode.production = false
a mód se ti bude nastavovat už při
Environment::loadConfig()
.
…
if (Environment::getName() == Environment::DEVELOPMENT) { Debug::enableProfiler(); }
To bych nedělal. To bude fungovat jen v případě, že používáš právě tyto dvě prostředí. Jakmile budeš potřebovat více prostředí, narazíš na problém.
- romansklenar
- Člen | 655
David Grudl napsal(a):
Můžeš si pro každé prostředí dát do config.ini
[production] mode.production = yes [development] mode.production = false
a mód se ti bude nastavovat už při
Environment::loadConfig()
.
Tím se to hezky zjednodušuje, mělo mě trknout podívat se do examples :)
To bych nedělal. To bude fungovat jen v případě, že používáš právě tyto dvě prostředí. Jakmile budeš potřebovat více prostředí, narazíš na problém.
Na tom je to založené, zatím mi stačila tyto dvě prostředí. Jde spíš jen o takovou automatizaci, kdy hodně často měním tyto dva režimy a testuju, ať nemusím přepisovat nebo komentovat na více místech.
Možná bude lepší ale použití
if (!Environment::isProduction()) { Debug::enableProfiler(); }
Editoval romansklenar (14. 9. 2008 20:59)
- romansklenar
- Člen | 655
Nevíte někdo jak vyhazovat/zpracovávat vyjímky při zaplém output bufferingu z úvodního postu tak, aby k ukončení aplikace a nezůstala viset na prázdné bílé obrazovce a zároveň aby se logovaly? (logování mám vyřešeno v ErrorPresenteru, tzn stačilo by mi to předat nějak na něj, s vyplým output bufferingem to vše funguje, vyjímka se předá a zpracuje v ErrorPresenteru). Any ideas?
- xificurk
- Člen | 121
romansklenar napsal(a):
Nevíte někdo jak vyhazovat/zpracovávat vyjímky při zaplém output bufferingu z úvodního postu tak, aby k ukončení aplikace a nezůstala viset na prázdné bílé obrazovce a zároveň aby se logovaly? (logování mám vyřešeno v ErrorPresenteru, tzn stačilo by mi to předat nějak na něj, s vyplým output bufferingem to vše funguje, vyjímka se předá a zpracuje v ErrorPresenteru). Any ideas?
Imho to nebude chyba output bufferingu. Mně to funguje naprosto v pohodě. Nejčastěji jsem na bílou obrazovku smrti narazil, když se mi vyskytla chyba, která spustila přesměrování na ErrorPresenter a při tomto přesměrování nastala další chyba.
- romansklenar
- Člen | 655
Fakt že jo, chyba byla někde jinde… Kde to ale netuším, protože jsem si s tím hoďku hrál tak dlouho až jsem to celé překopal, že nezůstal stát na kameni kámen a nepodařilo se mi postřehnout co byla ta klíčová změna.
- phx
- Člen | 651
Zdravim vas…
Mam problem. Ve startup()
zapnu ob_start();
dale v
shutdown()
si stahnu data z bufferu
$content = ob_get_flush();
a nejak s nima pracuji.
Vse je OK do chvile kdy dojde na strance k presnerovani. V tu chvili mi to
vyhodi vyjimku, ze nemuze modifikovat hlavicky, protoze radek s
$content = ob_get_flush();
pry odeslal nejaky vystup.
Kde je problem?
Dalo by se to obejit, kdyby slo nejak detekovat zda nedoslo k presmerovani, ale jak na to jsem neprisel.
- romansklenar
- Člen | 655
A kde dojde k volání toho přesměrování?
Pokud přesměrování pořešíš před zapnutím OB (pokud to tvoje situace umožňuje) tak by ses mohl vyhnout problému.
Už si nepamatuju jak, ale tohle jsem taky řešil, a pro práci s OB používám potomka presenteru, který je na to určený (řešení defakto stejné jako publikoval xificurk).
- phx
- Člen | 651
Dle doporuceni jsem zacal kesovat ve startup()
, driv to snad
krome konstruktoru nejde. A resit logiku pohledu ve startup se mi nechce.
Nebo je vhodne OB zapnout az na konci prepareXzy()
nebo
beforeRender()
? Logicke by mi prislo kesovat od
beforeRender()
po afterRender()
, ale pokud se nepletu
tak v dobe volani afterRender jeste neni vyrenderovan cely layout, ale
jen scena.
Pokud myslis https://forum.nette.org/…iewtopic.php?… tak bohuzel uz to nejde stahnout:(( Nemas to nekde?
- kravčo
- Člen | 721
phx napsal(a):
Nedari se… Vpodstate to mam stejne. Zapnu OB, vypnu OB ziskam data a vypisu. Pri redirectu zadna data nejsou. Overeno:
$content = ob_get_flush(); echo "'".$content."'"; // vypise '' die();
Tak at mi to netvrdi, ze byl nejaky vystup vygenerovan!!! Uz me napada jedine BUG v PHP.
prečo ob_get_flush()? nechcelo by to skôr ob_get_clean()?
Podľa PHP dokumentácie ob_get_flush() okrem toho, že vráti buffer, tak ho aj pošle prehliadaču… Tipujem, že aj keď posiela prázdny reťazec, posiela pred ním hlavičky…
- kravčo
- Člen | 721
phx napsal(a):
Vse je OK do chvile kdy dojde na strance k presnerovani. V tu chvili mi to vyhodi vyjimku, ze nemuze modifikovat hlavicky, protoze radek s
$content = ob_get_flush();
pry odeslal nejaky vystup.
Narážal som tým najmä na toto, predtým som neúplne citoval…
Totiž ob_get_flush()
výstup odosiela, po jeho volaní sú už
hlavičky odoslané.
// nejde $content = ob_get_flush(); // jde $content = ob_get_clean(); echo $content;
Tu súhlasím, totožné by to malo byť, ale len v prípade, že medzi
získaním z ob_get_clean()
a vypisovaním sa nič iné
nevykonáva…
- _Martin_
- Generous Backer | 679
Těžko říct takhle, co se tam přesně děje, ale doporučuji používat fci ob_get_clean() – jinak by jsi – pokud dojde k přesměrování na další kešovaný presenter – spustil v metodě startup nové bufferování, aniž by jsi předchozí ukončil. Navíc se to může hodit, pokud chceš používat kompresi přenášených dat (třeba metodou enableCompression() třídy HttpRespons).
- phx
- Člen | 651
_Martin_ napsal(a):
Těžko říct takhle, co se tam přesně děje, ale doporučuji používat fci ob_get_clean() – jinak by jsi – pokud dojde k přesměrování na další kešovaný presenter – spustil v metodě startup nové bufferování, aniž by jsi předchozí ukončil.
Novy request po presmerovani by nemel ovlivnit predchozi, ohledne OB. Kazdy reqest zacina s cistou pameti, krome session:) Leda ze myslis vnitrni Nette predani.
- _Martin_
- Generous Backer | 679
phx napsal(a):
Novy request po presmerovani by nemel ovlivnit predchozi, ohledne OB. Kazdy reqest zacina s cistou pameti, krome session:) Leda ze myslis vnitrni Nette predani.
Přesně, špatně jsem se vyjádřil, myslel jsem „přesměrování“ pomocí ForwardingException.
- David Grudl
- Nette Core | 8218
Mezi ob_get_flush()
a ob_get_clean()
je zásadní
rozdíl – clean obsahu bufferu jen vrátí, flush do vypíše a vrátí.
Chybou je, že jsem v uvedeném řešení s přesměrováním nepočítal. Nenapadlo mě to. Šlo by to upravit takto:
protected function shutdown()
{
if ($this->cacheKey) {
$content = ob_get_clean();
if ($content) {
echo $content;
...
}
}
}
Ale ani to není úplně ideální – cache by totiž neměla v případě chyby nic ukládat. Takže jsem upravil Presenter, aby metodě shutdown() předával případnou zachycenou výjimku.