Jak zrychlit Web(trh)

Posledních několik dní jsem, povzbuzený přestěhováním na nový server, zrychloval načítání Webtrhu.

Reakční doba
Náš dojem z aplikace se mění podle reakční doby:

Pod 0,1 s vnímáme reakci jako okamžitou. Aplikace reaguje na naše požadavky, přímo ji ovládáme.
Pod 1 s ztrácíme pocit okamžitosti, ale prodleva nepřerušuje naši práci.
Prodleva pod 10 s přerušuje uživatelovu práci, ale ten ještě dokáže udržet pozornost na úkol.
Reakce trvající déle než 10 s ztrácí lidskou pozornost.

Cíl je jasný: Reagovat pod desetinu sekundy.

Jaká kritéria optimalizovat
1. Co nejméně požadavků
Každý požadavek je drahý. Může obsahovat řadu zdržujících činností od překladu DNS, zahájení a ukončení TCP [neřkuli SSL] spojení po zbytečné chybové (4xx) a přesměrovávací (30x) stavy.
Je vhodné požadavky spojit, šetřit spojení – udržovat je otevřené pomocí Keep-Alive – a opakované požadavky úplně odstranit správným cachováním.

2. Rychlé odpovědi
Požadavky musí být vyřízené co nejdřív. Server je musí zpracovat rychle, výstup odeslat co nejdřív a odpověď musí cestovat co nejrychleji.

3. Malé odpovědi
Všechny odpovědi je důležité gzipovat a kód – HTML, JS, CSS – předtím minifikovat, pokud je to možné.
U kódu se objem přenášených dat po minifikaci a gzipování zmenší většinou na méně než 20%.

4. Feedback co nejdřív
Často můžeme dát uživateli feedback ještě před odesláním požadavku. Reakční doba je pak rozložená na okamžitý feedback a opožděné zobrazení odpovědi. Neurychlí se tím samotné provádění úkolu, ale zlepší se dojem z používání aplikace.

MySQL
Vlastně jsem na začátku neříkal úplně pravdu. Optimalizovat jsem začal už v zimě, kdy jsem se zakousl do užitečné knihy High Performance MySQL.

Autoři měli naprostou pravdu, když tvrdili, že správně navržené indexy a napsané dotazy dokáží urychlit zpracování dat o řády. Hlavní stránku Webtrhu jsem přepsáním několika dotazů a úpravou indexů zrychlil z několika sekund na desetiny.

Optimalizace nastavení SQL serveru a cachování výkon vylepšila přesně podle jejich předpovědi už „jen“ v desítkách procent.

Pokud má aplikace problémy v DB, je dobré začít právě tam. Optimalizace DB poskytne nejvíc muziky, protože nejvíc ovlivňuje Time To First Byte.

Server
Všechny odpovědi by se měly gzipovat a měly by mít správné hlavičky pro cachování. V Apache jednoduše pomocí mod_expires a mod_deflate:

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css\
 text/xml application/x-javascript text/javascript application/javascript
</IfModule>

<IfModule mod_expires.c>
  ExpiresActive on
  ExpiresDefault                          "access plus 1 month"
  ExpiresByType text/html                 "access plus 0 seconds"
  ExpiresByType text/xml                  "access plus 0 seconds"
  ExpiresByType application/xml           "access plus 0 seconds"
  ExpiresByType application/json          "access plus 0 seconds"
  ExpiresByType application/rss+xml       "access plus 1 hour"
</IfModule>

Víc komentované konfigurace je v ukázkovém .htaccess HTML5Boilerplate.

Až budeme cachované soubory měnit, klienty přinutíme stáhnout si novou verzi souboru změnou jména. Stačí upravit query za otazníkem:
external.js?20110721

Nezapomeňte zkontrolovat zapnuté Keep-Alive (Apache má defaultně zapnutý) pro udržování otevřených spojení.

Výstup bychom měli začít odesílat co nejdřív. Pokud se v requestu provádí nějaká údržba, je dobré postupovat takto:

  1. Vytvořit výstup
  2. Odeslat výstup
  3. Provést údržbu (zvýšení čítačů, přepočet statistik apod.)

Nebo ještě čistěji – přesunout údržbu do pravidelně volaných procesů.

PHP v továrním nastavení odešle výstup až na konci běhu skriptu. Dá se přinutit k okamžitému odeslání pomocí flush(); (jednorázově), ob_implicit_flush(); (po dobu běhu skriptu) nebo příslušným konfiguračním příznakem.

Této techniky jsem se musel zříct, protože ve vBulletinu se HTML výstup generuje až úplně nakonec. Můžeme ji ale využít třeba u AJAX požadavků.

Nainstaloval jsem taky APC, opcode cache pro PHP, nakonec s touto konfigurací:

apc.shm_size="128" ; velikost cache v MB.
  ; Pokud APC nepoužívá mmap, nastavit podle tohoto
apc.stat="0" ; APC přestává při každém požadavku ověřovat, jestli se soubor nezměnil.
  ; Pro staging server zapnout
apc.ttl="86400" ; životnost opcode cache
apc.user_ttl="86400" ; životnost user cache (proměnných)
apc.lazy_functions="1"
apc.lazy_classes="1"
 ; Dvě nové featury přidané vývojáři Facebooku, lazy loading funkcí a tříd

HTML
Požadavky jsou drahé, takže pokud je to možné, spojte všechny JS soubory do jednoho, všechny CSS do jednoho, a všechny obrázky do jednoho image spritu.

HTML je možné minifikovat pomocí CSSMin nebo YUI Compressoru, ale pozor na signifikantní whitespace. Na Webtrhu minifikace začala žrát odstavce (oddělené novými řádky), kdykoliv jsem editoval příspěvek.

Javascript
Javascript blokuje render a načítání kvůli document.write. Proto se dřív doporučovalo: Přesuňte JS až na konec stránky. Dnes existuje lepší metoda.

Přestaňte používat document.write a načítejte JS asynchronně pomocí knihovny jako LABjs nebo Require.js. Pro Webtrh jsem zvolil LABjs, nedělá nic jiného a gzipovaná má jen 2,2 kB.
Javascript se pak načítá mimo ostatní requesty na stránce a neblokuje je.

Kód musí být připravený pro minifikaci. Minifikátory jsou různě agresivní, od extra agresivního Dean Edwards Packeru přes hodně doporučovaný YUI Compressor až po mírumilovný JSMin od Douglase Crockforda.

Kód zabalený Packerem nefungoval správně a jelikož rozdíl mezi minifikátory je zanedbatelný, nevěnoval jsem se tomu a zvolil JSMin.

Překvapivé překážky při přeskládávání JS
(V nouzi pomůže aliterace. :)

Při spojování souborů do jediného se vyskytla chyba v syntaxi, protože poslední deklarace v jednom ze spojovaných souborů nebyla ukončená středníkem. První deklarace z následujícího souboru se tedy stala součástí předchozí deklarace a syntax error byl na světě:
callMe()callMeToo();

Jakmile začnete načítat všechny skripty na stránce asynchronně a zároveň máte ve stránce inline skripty, začnou okamžitě v konzoli vyskakovat chyby. Inline skripty se totiž najednou rozběhnou PŘED načtením zdrojů.
LABjs poskytuje řešení ve formě metody wait():

$LAB
.script('external.js')
.wait( function() {
  // na předchozím souboru závislé inline skripty
});


To s sebou ale přineslo několik neočekávaných záludností, které mě přiměly k emotivním commitům v Gitu. :)

Problém č. 1: Pokud je external.js načtený z cache a je dlouhý, inline skripty se spustí dřív, než se interpretuje kód z externího souboru.
Řešení: setTimeout(executeInlines, 1);

Problém č. 2: Inline skripty ztratí globální scope, na kterém závisí. Zároveň jsou deklarované jako privátní proměnné a nelze je uchopit výčtem
for(var prop in this) {}
Čisté řešení: Zbavit kód závislosti na globálním scope. V ideálním případě bych skripty přepsal a zbavil chybné závislosti na globálním stavu. Protože legacy kód má ale několik tisíc řádků, použil jsem regex/eval hack zmíněný dál. Šlo o záměrné rozhodnutí ponechat technický dluh, abych se nezdržel na několik dnů až týdnů.
Rychlé řešení: Projít inline kód regexpem a vybrat všechny názvy funkcí a lokálních proměnných. Předat je JS proměnné, a zkopírovat eval()em do globálního scope.
Brutální, ale funguje.

Problém č. 3: Protože se asynchronní skript načítá nezávisle, je nutné explicitně stanovit závislosti všech událostí.
Tohle zní nevinně, ale zabralo mi to celý den. vBulletin definuje dvě vlastní události, které se musí odpálit v určitém pořadí. Nikde to ale není explicitně napsané, natožpak nakódované.
Protože na sebe skripty a DOM přestaly čekat, události se spouštěly v náhodném pořadí.
Řešení: Identifikovat závislost a explicitně ji uvést do kódu:
Událost č. 2 musí počkat na událost č. 1.

Rozhraní
Pokud má akce jasný výsledek (jeden stav), můžeme zareagovat ještě před odesláním požadavku. Rozhraní se pak chová responzivněji, ačkoliv se reakční doba nezkrátila.

Okamžitý feedback jsem aplikoval v hlavním navigačním menu Webtrhu. Menu zareaguje na kliknutí ihned, ještě předtím, než se začne načítat nová stránka. Vyzkoušejte to.

Co dál
Zrychlování stránky se odehrává na mnoha úrovních a je to hodně zajímavá a uspokojující činnost. :)
Nepodařilo se mi zatím ale dosáhnout původního cíle a zrychlit stránky pod desetinu sekundy.

Jak zrychlit načítání dál? Použít pro statický obsah doménu bez cookies. Zvětšit initial TCP congestion window. Dál optimalizovat databázi. Agresivněji cachovat části stránek, alespoň pro nepřihlášené návštěvníky.

Jaké máte další nápady?

Další čtení

Příspěvek byl publikován v rubrice Nezařazené. Můžete si uložit jeho odkaz mezi své oblíbené záložky.

11 komentářů u Jak zrychlit Web(trh)

  1. Milan K. napsal:

    Můžeš vyzkoušet reverzní proxy cache nginx …

    • Martin napsal:

      O Nginxu taky uvažuju. Vlastně nevím, jakou část z požadavku teď tvoří práce na straně Apache. Musím se na to podívat. Díky za připomenutí.

  2. Milan K. napsal:

    A ještě pro minifikaci JS kódu a CSS můžeš zkusit nástroj od Seznamu:

    http://opensource.seznam.cz/KJScompress/index.html?lang=cz

  3. Bob napsal:

    Ahoj,

    otázka stranou: Jak je to v současné době s výkonem MySQL a Postrgre? Uvízl jsem u Oracle, takže už nevím, jak se věci mají. Jde vůbec u phpBB snadno přejít od jedné DB k druhé?

  4. Martin napsal:

    Směl bych dotaz, na jakém HW teď jede Webtrh? Jen tak pro zajímavost, s jakým náporem se server potýká. :)

  5. nginx napsal:

    A co prejit na nginx místo Apache? Nebylo by to o neco rychlejsí?

    • Rowell napsal:

      Pěkně shnurté. Je pravda, že spousta lidí zapomíná na ty popisky u tagů (img, a,…) a co jsem tak v poslední době četl na SEOmozu a jinde, tak právě google dává velkou váhu na název obrázku a popisky.Je tento komentář užitečný? 0  0

  6. Carl114 napsal:

    Hezký článek. Líbí se mi jak je obsáhlý. Nejvíc mi asi přijde vhod část o JS. V dnešní době už je běžné, že některé stránky mají 200-400 kB JS a ty pak brzdí načítání a lidi koukají na bílou plochu. A prvky jako defer v HTML5 nejdou použít skoro nikde takže co pak…

    Docela dost se o zrychlování stránek také zajímám a píšu různé návody, ale okolo JS jsem nic obstojného a zároveň jednoduchého ještě nenašel. Ale jak koukám tak bez zasahování do kódu skriptu se to neobejde. Díky za zdroje.

  7. cirda napsal:

    Jak moc používáš cachování?
    Nemyslím tím cachování jaké má nette či jiné frameworky.
    Dokáže to zrychlit web taky několikrát. Kešuj si web přímo do ramky.
    Každá stránká má svoji kešku a pokud se změní změní tak se keš přepíše na novější. Umístěno je to přímo v RAM paměti takže nenačítáš žádné sql dotazy ani žádné html se nikde negeneruje. Prostě to pošle rovnou výsledek. Můžeš tak kešovat buď rovnou výstupy PHP a Mysql parserů čili čisté html nebo pouze některé nejčastější sql dotazy, tkeré zabírají moc paměti. Ale nejlepší je kešovat vždy celé html stránky a to třeba pouze ty aktuálně nejnavštěvovanější.
    Nejde kešovat úplně vše to by se nevlezlo do RAMky ta má omezenou kapacitu ale tímto způsobem lze vlemi urychlit chod stránek.
    To cos psal předtím je takový základ pouze na začátek. I když jsem nevěděl vše cos psal a za to ti díky. Už se tomu zas tolik nevěnuji, ale hodně toho co tam máš by se mělo používat běžně. Ovšem ne každý vývojář je ochotný se piplat s těmi postupy zabere to více času, který se kvůli tomu prodlouží u vývoje webu.

    Hmm po dlouhé době jsem zas něco napsal k webovému vývoji :) Hezký den

    • Martin napsal:

      Jak píšu v článku, používáme APC na úrovni PHP a MySQL má svou cache.
      Cachovat celý HTML výstup můžeme jako komunitní web jen pro nepřihlášené návštěvníky. Pro přihlášené cachujeme jen některé stavební bloky (na úvodní straně např.).

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

*

Můžete používat následující HTML značky a atributy: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>