Zrychlete svoji webovou aplikaci pomocí Partial Update
Partial update neboli částečná aktualizace stránky (pomocí AJAXu) není technika zrovna nová. Po pravdě řečeno však stále není běžná, přestože její správné použití může velmi pozitivní dopady na celkový výkon systému a také je velmi dobře přijímána uživateli. Na otázku proč, můžeme odpovědět problematickou podporou ve frameworcích - některé se na jedné straně snaží o maximální odstínění programátorů od JavaScriptu, čímž z dané techniky dělají věc více méně magickou - jinde naopak použití vyžaduje větší než malé znalosti "skriptování", což zase většinu Javistů, paradoxně, vyřadí ze hry.
V tomto článku si vysvětlíme základní principy, které jsou s touto technikou spojovány a ukážeme si je na příkladech živé aplikace. Způsob jakým jsme tuto techniku implementovali my, by se měl přibližovat k vlastnímu jádru věci, takže by vám měl být princip zřejmý.
Webové aplikace pracují typicky tak, že klient (browser) pošle v requestu data, server sestaví výslednou HTML stránku a vrátí ji zpět browseru, který ji následně interpretuje = vykreslí. Partial update na tomto způsobu komunikace mění jen 2 věci:
- request probíhá na pozadí pomocí AJAXu,
- server nesestavuje kompletní stránku, ale jen její část.
Vlastní aktualizace obsahu stránky zobrazované uživateli se, po přijetí response, provede jednoduchou manipulací s DOMem (tj. objektovou reprezentací HTML stránky) pomocí JavaScriptu.
Přiklad partial update v praxi na našem projektu. (klikněte na Záložku poptávky, procházejte seznam přes stránkování dole)
Celé to funguje velmi jednoduše. Náš webový framework je postavený na komponentovém principu a tudíž přesně víme, který kus HTML renderuje jaká komponenta. Jednoduše se to dá prezentovat na následujících odkazech:
- Komponenta zobrazující filtrování
- Komponenta zobrazující tabulku s výsledky
- Komponenta zobrazující stránkování
- Komponenta zobrazující kompletní obsah středové části
Schopnost přesně ohraničit části stránky, které chceme potenciálně "částečně vykreslovat" je jedna z nutných podmínek použití partial update ve vaší aplikaci. Na serverové části musíte používat knihovny, které vám toto umožní. Velkou výhodu v tomto mají komponentově orientované frameworky, které ze své podstaty toto ohraniční komponent a jejich výstupu mají. V případě MVC frameworků se podobná záležitost dá simulovat pomocí templatovacích knihoven jako jsou např. Tiles.
MVC frameworky mají ovšem situaci komplikovanější i v dalších ohledech. Schopnost vykreslit jen část výstupu totiž není to jediné, co hraje roli. Druhým velmi podstatným faktorem je umět jednoduše omezit aplikační logiku vykonávanou ve spojitosti s vykreslovaným výstupem - tj. nedotazovat se aplikační vrstvy na něco, co v částečně renderovaném výstupu nebude potřeba.
Komponentové frameworky obvykle využívají patternu View helper - což zjednodušeně znamená, že view v rámci vykreslování přímo volá "aplikační logiku". MVC pattern vykonává aplikační logiku již na úrovni controlleru, který je zároveň zodpovědný za přípravu všech dat potřebných pro view (tedy vykreslování) - při partial updatu musíte sami oprogramovat a držet v povědomí, co bude ve view kdy potřeba a co ne. S view helperem tento problém neřešíte - co si view vrstva při vykreslování nezavolá, to se neprovede. View helper přináší na druhou stranu zase problémy spojené s cachováním výstupů aplikační logiky. Jednoduše se totiž stane, že z view zbytečně voláte jednu metodu se stejnými parametry mnohokrát, aniž byste si toho všimli (zvlášť když máte stránku složenou z mnoha nezávislých komponent).
V případě, že jste schopni na straně serveru splnit tyto dvě podmínky, je implementace na straně klienta již velmi jednoduchá. V našem případě jsme vytvořili jQuery plugin, který dokáže projít všechny odkazy a submit tlačítka v konkrétní oblasti (ohraničené jak jinak než komponentou) a přidat jim "onclick" funkce, které při kliknutí na odkaz / submit, místo refreshe kompletní stránky, provedou odpovídající AJAXový dotaz na server s následným partial updatem:
$("#partialUpdateCnt").partialUpdate();
Ekvivalentní zápis, pokud bychom volání chtěli zapisovat k jednotlivým linkům ručně, by vypadal takto:
//první parametr je url pro partial update,
//druhý parametr je ID DOM objektu jehož obsah se má vyměnit po odpovědi serveru
<a href="..." onclick="return $.partialUpdate.execute(this.href, 'partialUpdateCnt');"/>
Pokud bych odstranil všechen balast a šel až na kost, dostal bych něco takového:
<a href="..."
onclick="$.post(
this.href + '@partialUpdateCnt',
function(data) {
$('#partialUpdateCnt').replaceWith(data);
});
return false;
"/>
Vlastní komunikaci si můžete jednoduše zobrazit na odkazovaném příkladě pomocí Firebugu, popřípadě jiných vývojových nástrojů. Firebug je pro monitoring této výměny dat naprosto ideální - hezky v něm uvidíte veškerý obsah request i response serveru. Všimněte si také časů, za které je server schopný vygenerovat odpověď.
Pozitivní efekty partial update
Snížení zatížení serveru
Zavedením partial update snížíte zatížení serveru hned ve dvou ohledech:
- omezení vykonávané aplikační logiky
Server neprovádí nic navíc kromě vlastní akce vyvolané uživatelem (např. uložení dat) a vrácení dat pro aktuálně vykreslovanou oblast. Tím se ušetří obrovské množství výkonu - jen se sami na svém aktuálním projektu zamyslete, na co všechno se musíte dotázat aplikační vrstvy, abyste dokázali vykreslit celou HTML stránku - přitom většina věcí se, z pohledu uživatele, mezi jedním a druhým requestem nemění!
- snížení objemu dat pro přenos mezi webserverem a klientem
Kromě toho, že část stránky bude mít vždy menší velikost než stránka celá, je důležité především to, že v této malé části je také daleko menší počet linkovaných objektů (skripty, styly, obrázky), na které se musí browser zpětně dotázat serveru (i kdyby měl dostat zpět HTTP 304 NOT MODIFIED). Menší počet linkovaných objektů = menší počet HTTP requestů na web server = menší zatížení web serveru jako takového.
Řádové zrychlení odezvy uživateli = lepší user experience
Snížením zátěže serveru a snížením objemu přenášených dat a počtu HTTP požadavků na linkované zdroje, partial update skutečně velmi výrazně (řádově) zvyšuje odezvu uživateli aplikace. Odlehčením serveru dostáváte také daleko větší propustnost (throughput) aplikace - tj. více paralelních uživatelů.
Ve vlastní rychlosti hraje velmi důležitou roli i vlastní výměna DOMu - browser má výrazně méně práce sestavit pouze část stránky, než stránku celou. Při vykreslování celé stránky musí browser znovu zpracovat všechny CSS, při prvním načtení (body.onLoad / $(document).ready()) se provádí řada inicializačních procedur linkovaných javascriptových knihoven atd. Obvykle se tak v browseru nevyhnete bliknutí obsahu stránky (porovnejte s přechody mezi statickými stránkami poskytovanými Apachem - klikněte např. na Otázky a odpovědi nebo Výherci). Při částečných výměnách DOM obsahu k podobnému "nepříjemnému" efektu nedochází.
A co se týká "user experience"? Hádejte jestli bude uživatel více spokojen s aplikací, která bude mít odezvy do 1 sekundy, nebo takovou, kde si počká 3-4 vteřiny na vykreslení stránky? Schválně si naměřte odezvy dané části Roku jinak - odpovědi serveru na partial updaty jsou mezi 100 až 150 milisekundami.
Problémy spojené s partial updatem
Správná implementace partial update má ovšem i svoji cenu. Existují komplikace, které vám nasazení partial update do vaší aplikace ztíží.
Zajištění správného kontextu - FORM, ANCHOR
Na frontendové části musíte ošetřit různé kombinace sběru dat odesílaných na server dle kontextu. Pro jednoduché odkazy stačí vzít HREF a přes jQuery.get (popřípadě post) dotázat server. V případě formuláře musíte serializovat data vyplněná ve formulářových políčkách a ty odesílat jako parametry. jQuery má pro toto jednoduchou metodu:
var data = $("#formId").serializeArray();
Omezení počtu AJAX požadavků na server
V případě, že uživatel kliká příliš rychle, nebo partial update posadíte na události typu "onmouseover" je dost možné, že se v jednu chvíli seběhne hned několik volání na server. Pro server se jedná o nezávislá volání, takže je možné že pozdější dotaz je vyřízen dřív, popř. že různé odpovědi přijdou klientovi více méně současně. To způsobuje značnou řadu problémů - jednak může klient vidět jinou odpověď než aktuálně čeká (odpovědi se předběhly), popřípadě dochází k chybám při výměně DOM struktury, pokud odpovědi přijdou velmi krátce po sobě (rychlé výměny DOM objektů browserům příliš nesvědčí). Kromě těchto problémů, také velké množství AJAX requestů zbytečně zatěžuje server, aniž by to uživateli něco přinášelo (typicky uživatel chce jen poslední z nich).
Tenhle problém je už trošku větším oříškem. My jsme jej vyřešili integrací jQuery Ajax Manager pluginu, který umí inteligentně spravovat frontu ajaxových požadavků. Jednak zajistí sériovou komunikaci se serverem, cachování odpovědí pro stejné dotazy, ale také dokáže z fronty vylučovat duplikátní dotazy, které jsou v podstatě zbytečné. Víc detailů je k dispozici na homepage pluginu.
Kromě výše uvedeného je nutné také uživateli dát najevo, že systém zareagoval na jeho kliknutí (či jinou operaci). V opačném případě si uživatel může myslet, že něco provedl špatně, že se "nic neděje" a kliká zbytečně dál. Toto jsme vyřešili zobrazením animovaného gifu poblíž kurzoru myši, který kopíruje jeho pohyb až do doby, než doběhne proces částečné aktualizace stránky. Po kliknutí tedy uživatel ihned vidí, že systém na pozadí pracuje a už dál nekliká. Navíc jsou uživatelé na animované GIFy v souvislosti s AJAXem zvyklí, takže není třeba nic dál vysvětlovat a podvědomně uživatelé chápou, co se děje.
Graceful degradation
V souvislosti s Partial update je nutné také myslet na uživatele s vypnutým JavaScriptem. Možná si řeknete, že ty 2-3 procenta uživatelů směle oželíte, ale
- každé procento uživatelů je důležité
- je to záležitost cti ;-)
U nás ve Forrestu ctíme pravidlo, že i uživatel s vypnutým JavaScriptem musí být schopný "nějak" zobrazit všechny informace na webu a musí být také schopen provést všechny základní akce (úkony), které s webem souvisí (však si zkuste pustit RokJinak s vypnutým JavaScriptem ;-) ).
Pokud implementujete partial update chytře, nebude pro vás graceful degradation žádný problém. Typický vývoj u nás spočívá v tom, že se vytvoří verze bez partial update - tj. každý klik / submit znamená obnovení celé stránky. Pak jen dynamicky doplníme ke všem linkům / submitům v konkrétní oblasti "onclick" handlery, které provedou její partial update. Na server se posílá vždy kompletní informace, takže serverová část je vždy "plně v obraze" co se děje na klientovi - v žádné chvíli tedy nedochází k tomu, že by se stav na serveru a na klientovi rozešel.
Důležité je, že:
- vše funguje i bez použití JavaScriptu
- nevyvíjí se dvě nezávislé větve kódu, které se musí udržovat
Stačí původní kód obohatit jediným voláním JavaScriptu:
$("#doplnIdKomponenty").partialUpdate();
Srovnejte náš přístup srovnáte se standardními Ajax komponentami - namátkou třeba DataGridy, které typicky používají nějaký svůj proprietární "protokol" pro komunikaci s backendem na pozadí. Zkuste si v následujících ukázkách vypnout JavaScript a uvidíte, jak dopadnete:
- ICE Faces komponenty
- JustAjax DataGrid
- ActiveWidgets gridy
- Ext Livegrid
- AjaxDaddy tables
- Jedině Wicket funguje i s vypnutým Javascriptem, což už se bohužel zase nedá říct o této ukázce s Wicketem, takže #fail
Historie navštívených stránek
Poslední věcí, kterou je nutné brát v úvahu, je údržba správné historie stránek při partial updatech. Částečná aktualizace obsahu se samozřejmě do historie browseru nezapisuje a tak ve chvíli, kdy uživatel vynutí refresh stránky (reload), popřípadě dá Zpět, neberou se tyto změny obsahu v úvahu. Vůči uživateli to není úplně v pořádku, protože pokud máme například scénář - zobrazení listovacího seznamu, přechod na X stránku (partial update), kliknutí na detal položky (přesměrování na novou stránku), a stisknutí tlačítka zpět, uživatel se dostane zpět do listovacího seznamu na první a ne na Xtou stránku, ze které kliknul na detail položky. Obdobně pokud postupně v listovacím seznamu prošel na 5 stránku a dá reload, nezůstane na 5. stránce, jak by čekal, ale na stránce první.
Ovlivňovat historii prohlížeče z Javascriptu v více méně nejde - jednak neexistuje API, a to co máme k dispozici povede k refreshi celé stránky, čímž ztratíme výhody partialUpdate. Existuje jediná možnost, jak si s tímto problémem poradit a tím je využití tzv. "hash" části url. V následujícím příkladě se jedná o string "kotva":
http://www.domena.cz/stranka.html#kotva?parametr1=hodnota1
Hash část url se v původním smyslu používá pro navigaci mezi různými částmi stránky. My jej ovšem můžeme využít také pro obejití nedostatečného API pro práci s historií browseru. V případě, že pomocí JavaScriptu zapíšete hodnotu do window.location.hash, browser se pokusí na dané stránce najít element s tímto ID, a pokud jej najde, pokusí se na něj zascrollovat viewport. Pokud jej nenajde, nestane se nic - pouze se změní url v adresní řádce (doplní se tam požadovaný hash) a toto nové změněné url se také zapíše do historie procházení. Důležité je to, že se v tomto případě něděje žádná komunikace se serverem a vše se odehrává pouze na klientovi.
Této techniky využívá řada jQuery pluginů pro správu historie stránek. Např. jquery.history.js plugin nebo Really Simple History. Problém tohoto a podobných pluginů tkví v tom, že jsou optimalizované pro jedno-stránkovou AJAX aplikaci, jakou je například GMail (vše se děje v jednom okně) a využívají pro monitoring historie skrytý iframe na dané stránce. Při přechodu na jiné url se tedy historie ztrácí.
Pro naše účely jsem napsal vlastní kód, který staví na stejném principu. Při požadavku na partial update dané stránky si uložím parametry jdoucí na server do hash části url (enkóduji nebezpečné znaky a dávám jednoznačný prefix). Takto se mi do historie dostane nový záznam s tímto hashem. Pokud uživatel stiskne tlačítko zpět zabere kód na "onLoad" stránky kontrolující právě tento hash, který případně provede reload stránky s parametry vytaženými právě z tohoto hashe. Je to jedna otočka na server navíc, ale uživatel dostane verzi stránky jakou očekává.
Výše uvedený princip pracuje s různými odchylkami ve všech prohlížečích (testováno IE6+, FFox 1.5+, Chrome). V novějších prohlížečích pokud uživatel zmáčkne tlačítko zpět na stránce, na které před tím provedl partial update, není vyvolán "onLoad" dané stránky (mění se pouze hash, url zůstává stejné) a proto v tuto chvíli naše logika nezabere. Pro tuto situaci je na stránce v určitém intervalu (500ms) spouštěný skript kontrolující změnu hashe aktuálně prohlížené stránky, který v daném případě vyvolá logiku, která se jinak provádí jen při "onLoad". V IE6 zase není do historie zapisována posloupnost hashů na stejné stránce, ale vždy se zaznamená pouze poslední hash stránky.
Přiznávám, že tahle část není úplně triviální, ale nakonec funguje docela dobře :-) .
Odkazy
Partial update není náš výmysl a je implementován s různými odchylkami také v řadě jiných web frameworků. Namátkou vybírám některé z nich:
Bohužel se mi nepodařilo najít žádný jednoznačný článek pojednávající o implementaci partial update v JSF, přestože bych tuto podporu zde očekával. Budu vděčen za případné doplnění v komentářích. Zajímavým měřítkem kvality aplikace je úroveň ke graceful degradation - v současnosti je bohužel velmi tristní.
Komentáře