Pomalý web stojí peníze. Není to poučka do prezentace — každá vteřina navíc při načítání znamená lidi, kteří odejdou dřív, než vůbec uvidí, co nabízíte. Google navíc rychlost měří přímo v prohlížečích reálných návštěvníků a promítá ji do hodnocení ve vyhledávání. Rychlost proto není kosmetika na konec projektu, ale vrstva, která se táhne od serveru až po poslední obrázek na stránce. V tomhle článku ji projdeme celou — s příkazy a hlavně s vysvětlením, PROČ to dělat.

Rychlost jako byznys metrika: Core Web Vitals

Google měří tři takzvané Core Web Vitals, a vyplatí se je znát, protože přesně popisují, co uživatel vnímá. Hodnotí se vždy na 75. percentilu reálných uživatelských dat:

  • LCP (Largest Contentful Paint) — jak rychle se vykreslí největší prvek nad ohybem (obvykle hero obrázek nebo nadpis). Cíl: do 2,5 s.
  • INP (Interaction to Next Paint) — jak rychle stránka reaguje na kliknutí a psaní. Od března 2024 nahradilo starší FID. Cíl: do 200 ms.
  • CLS (Cumulative Layout Shift) — jak moc obsah „poskakuje“ během načítání. Cíl: 0,1 nebo méně.

LCP řešíte hlavně serverem a obrázky, INP JavaScriptem a CLS rozměry prvků. Pojďme na vrstvy.

Vrstva 1: Server

Komprese: gzip a hlavně Brotli

Textové soubory (HTML, CSS, JS, JSON, SVG) se dají zmenšit na zlomek. Brotli komprimuje o něco lépe než gzip, takže ho nasazujte jako první volbu a gzip nechte jako fallback pro starší klienty.

Nginx má gzip vestavěný:

gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml+rss;

text/html se komprimuje vždy automaticky, proto ho v seznamu nevidíte. Brotli ve výchozím balíčku Nginxu není — potřebujete modul ngx_brotli, který se zavádí jako dynamický modul:

load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;

Apache má oba moduly přímo v repozitáři Debianu 12 i Ubuntu 22.04:

sudo a2enmod deflate brotli headers expires
sudo systemctl reload apache2
<IfModule mod_brotli.c>
    AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml
</IfModule>
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
</IfModule>

Filtr DEFLATE navzdory svému názvu produkuje gzip výstup (Content-Encoding: gzip) — a vždy potřebuje seznam MIME typů, jinak ho Apache odmítne.

Nekomprimujte znovu už komprimované formáty — JPEG, PNG, WebP, AVIF a WOFF2. Ušetříte CPU a nic tím nezískáte.

HTTP/2

HTTP/2 multiplexuje víc požadavků a odpovědí přes jedno spojení, takže odpadá historický trik se slučováním všeho do jednoho balíku. Sám o sobě HTTPS nevyžaduje (specifikace zná i nešifrované h2c), ale prohlížeče ho podporují jen přes TLS — v praxi ho tedy zapínáte spolu s HTTPS.

Na distribučních verzích Nginxu (1.22 v Debianu 12, 1.18 v Ubuntu 22.04) je správně:

listen 443 ssl http2;

Od Nginxu 1.25.1 je ale zápis listen ... http2 zastaralý (nginx -t vypíše varování) a HTTP/2 se zapíná samostatnou direktivou — listen 443 ssl; plus http2 on;. Pokud si instalujete novější Nginx z nginx.org, používejte rovnou tuhle formu. Apache:

sudo a2enmod http2
Protocols h2 http/1.1

Cache hlavičky pro statiku

Statické soubory (CSS, JS, fonty, obrázky) se nemění — pokud používáte hashované názvy typu app.4f3a2b.css, můžete je klidně nechat cachovat na rok. Při změně obsahu se změní hash, tím i URL, a prohlížeč si automaticky stáhne novou verzi. immutable navíc říká prohlížeči, že se soubor po dobu své platnosti (max-age) nezmění, takže ho při běžném reloadu nemusí znovu ověřovat.

Pozor: samotný HTML dokument, který na hashovanou statiku odkazuje, se dlouhodobě cachovat nesmí — musí se revalidovat (no-cache nebo krátký max-age), jinak by klient držel staré názvy souborů a novou verzi by nikdy nenačetl.

Nginx:

location ~* \.(?:css|js|woff2|jpg|jpeg|png|webp|avif|svg|ico)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Apache (modul headersHeader set sám o sobě mod_expires nepotřebuje):

<FilesMatch "\.(css|js|woff2|jpg|jpeg|png|webp|avif|svg|ico)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

PHP OPcache

Pokud běžíte na PHP, tohle je nejlevnější zrychlení, jaké dostanete. OPcache drží zkompilovaný bytecode v paměti, takže se skripty nekompilují při každém requestu znovu. Konfiguraci najdete podle verze — /etc/php/8.2/fpm/php.ini na Debianu 12, /etc/php/8.1/fpm/php.ini na Ubuntu 22.04:

opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60

realpath_cache_size=8192K
realpath_cache_ttl=600

revalidate_freq=60 znamená, že PHP kontroluje časová razítka souborů nanejvýš jednou za 60 sekund — výchozí hodnota jsou pouhé 2 sekundy, což na produkci zbytečně zatěžuje disk. Hodnoty výše sedí na menší a střední weby; u velkých frameworkových aplikací (Laravel, Symfony) klidně zvedněte memory_consumption na 256 MB. max_accelerated_files by měl být vyšší než počet PHP souborů projektu (interně se zaokrouhlí nahoru na nejbližší prvočíslo).

realpath_cache ukládá výsledky kanonizace cest — rozřešení symlinků a relativních částí (./, ../) na kanonické absolutní cesty souborů i adresářů. U frameworků s tisíci require to citelně sníží počet stat dotazů na disk. realpath_cache_size je na PHP 8.x výchozně 4096K, takže hlavní přínos je v prodloužení TTL — ten ve výchozím stavu žije jen 120 sekund, což je pro produkci zbytečně krátce; u opravdu velkých aplikací se vyplatí zvednout i velikost, jak je výše na 8192K.

Po změně restartujte FPM a ověřte, že OPcache běží. Pozor — php -i čte konfiguraci CLI, ne tu, kterou používá web přes FPM; ptejte se proto přímo FPM binárky:

sudo systemctl restart php8.2-fpm
php-fpm8.2 -i | grep opcache.enable

Nejspolehlivější je stránka s phpinfo() servírovaná přes FPM, respektive opcache_get_status() (položka opcache_enabled).

Na produkci s atomickým deployem můžete nastavit opcache.validate_timestamps=0 (PHP pak nekontroluje změny souborů vůbec) — ale pak musíte OPcache po každém nasazení sami vyprázdnit, jinak poběží stará verze kódu.

Mikrocache a reverse proxy

Pokud generujete stejné HTML pro anonymní návštěvníky, nemusí ho aplikace počítat pořád dokola. Mikrocache v Nginxu uloží odpověď na pár sekund — i jedna vteřina utlumí špičku o řády, aniž byste řešili invalidaci:

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=MICRO:10m max_size=200m inactive=60m;

location ~ \.php$ {
    fastcgi_cache MICRO;
    fastcgi_cache_valid 200 1s;
    fastcgi_cache_use_stale updating error timeout;
    # ... standardní fastcgi_pass a fastcgi_params
}

Vrstva 2: Frontend

Obrázky jsou obvykle největší balvan

Obrázky bývají nejobjemnějším typem zdroje na stránce — u typické stránky kolem 40–50 % celkové váhy, u obrázkově náročných webů i víc. Tři pravidla:

  1. Moderní formáty. WebP a AVIF bývají při srovnatelné kvalitě obvykle výrazně menší než JPEG/PNG (WebP zhruba o 25–35 %, AVIF často ještě víc) — ne u každého obrázku, ale jako pravidlo se vyplatí. Nástroje jsou v repozitáři:
sudo apt install webp libavif-bin
cwebp -q 80 obrazek.jpg -o obrazek.webp
avifenc --min 20 --max 25 obrazek.jpg obrazek.avif

--min/--max (kvantizér na škále 0–63) platí pro libavif na Debianu 12 a Ubuntu 22.04. Novější libavif 1.x (Ubuntu 24.04+) je označuje za zastaralé a používá kvalitu na škále 0–100: avifenc -q 65 obrazek.jpg obrazek.avif.

  1. Správné rozměry. Neposílejte 4000px fotku do kontejneru širokého 600px. Použijte <picture> s fallbackem a vždy uvádějte width a height — z jejich poměru si prohlížeč ještě před stažením spočítá místo, které obrázku rezervuje, takže se obsah po jeho načtení neposune (zabíjí CLS). U responzivních obrázků k tomu v CSS ponechte height: auto:
<picture>
  <source type="image/avif" srcset="foto.avif">
  <source type="image/webp" srcset="foto.webp">
  <img src="foto.jpg" width="1200" height="800" alt="Popis" loading="lazy" decoding="async">
</picture>
  1. Lazy-loading přes loading="lazy" u obrázků pod ohybem. Pozor: hlavní hero obrázek (ten, co dělá LCP) nikdy nelazy-loadujte — zpozdili byste přesně tu metriku, kterou chcete zlepšit.

Minifikace a méně requestů

Minifikujte CSS i JS při buildu (esbuild, terser, Vite). Každý request má svou režii, takže méně a menších souborů = rychlejší start. HTTP/2 sice slučování zmírnil, ale nesmyslně roztříštěné desítky drobných souborů stále škodí — hledejte rozumný střed.

Kritické CSS

Prohlížeč nevykreslí nic, dokud nemá CSS potřebné pro viditelnou část stránky. Vytáhněte tenhle „kritický“ kus a vložte ho inline do <head>, zbytek CSS načtěte odloženě. LCP se posune výrazně dopředu, protože se odbourá blokující stažení celého stylopisu.

Jak výkon změřit

Nehádejte — měřte. Dva režimy:

  • Lab data (v laboratorních podmínkách): Lighthouse v Chrome DevTools nebo z příkazové řádky:
npm install -g lighthouse
lighthouse https://example.com --view
  • Field data (od reálných uživatelů): PageSpeed Insights čte data z Chrome UX Reportu (CrUX), takže vidíte, jak web vnímají skuteční návštěvníci. Pro detailní vodopád requestů a test z různých lokalit slouží WebPageTest.

Kompresi a cache hlavičky ověříte i bez nástrojů, přímo curlem. Použijte GET a nechte si vypsat hlavičky — na HEAD (-I) totiž některé servery Content-Encoding nevracejí:

curl -s -D - -o /dev/null -H 'Accept-Encoding: br' https://example.com/app.css | grep -iE 'content-encoding|cache-control'

Po každé změně konfigurace nezapomeňte na test syntaxe a reload:

sudo nginx -t && sudo systemctl reload nginx
# nebo
sudo apache2ctl configtest && sudo systemctl reload apache2

Shrnutí

Rychlost není jedno tlačítko, ale řetěz — a ten je tak silný jako jeho nejslabší článek. Na serveru zapněte Brotli s gzip fallbackem, HTTP/2, dlouhé cache hlavičky pro statiku s immutable (a nezapomeňte, že samotné HTML se dlouhodobě cachovat nesmí) a u PHP nasaďte OPcache. Na frontendu vyřešte obrázky (moderní formáty, správné rozměry, lazy-loading mimo hero), minifikaci a kritické CSS. A hlavně měřte — Lighthouse pro rychlou zpětnou vazbu, PageSpeed Insights a WebPageTest pro obraz z reálného provozu. Když vrstvy spojíte, LCP, INP i CLS se srovnají a s nimi konverze i pozice ve vyhledávání.