PageSpeed 100 na GitHub Pages — od 87 po perfektné skóre
Lighthouse ukázal Performance 87 na mobile. Self-hosting fontov, WCAG kontrast, trailing slash — a ako sa dá dosiahnuť 100/100/100/100 aj na GitHub Pages. Building in public, štvrtý diel.
Web má SEO na 100, Best Practices na 100 — ale Performance 87 a Accessibility 94. Lighthouse jasne ukázal, čo treba opraviť.
Východiskový stav
Po spustení PageSpeed Insights na rebjak.com/en som dostal:
| Metrika | Skóre |
|---|---|
| Performance | 87 |
| Accessibility | 94 |
| Best Practices | 100 |
| SEO | 100 |
Tri hlavné problémy:
- Google Fonts — render-blocking + externé requesty, ušetriteľných ~2000 ms
- Nedostatočný farebný kontrast — WCAG AA zlyhanie v dark aj light mode
- Trailing slash redirect —
/en→/en/stojí ~925 ms
1. Google Fonts → self-hosting
Problém
Klasický <link rel="stylesheet"> na Google Fonts blokuje renderovanie celej stránky, kým sa font CSS nestiahne. Na pomalšom pripojení to znamená bielu obrazovku na 1-2 sekundy navyše.
Aj keď sa render-blocking vyrieši cez preload, fonty sa stále sťahujú z externých serverov (fonts.googleapis.com, fonts.gstatic.com). Každý externý request znamená:
- DNS lookup — prehliadač musí preložiť doménu na IP adresu
- TCP + TLS handshake — nové spojenie pre každý server
- Žiadna kontrola nad cachingom — Google nastavuje vlastné cache hlavičky
Na mobile (simulované pomalé 4G s 150 ms RTT) to pridáva stovky milisekúnd navyše pri každom prvom načítaní.
Google Fonts servuje Inter ako variable font s veľkosťou 230 KB a JetBrains Mono 56 KB — spolu 286 KB cez externé servery.
Riešenie
Stiahol som fonty a subsettoval ich pomocou pyftsubset (z knižnice fonttools) na Latin + Latin Extended-A (U+0000-017F) — pokrýva angličtinu aj slovenčinu (č, š, ž, ľ, ď, ť, ň a ďalšie).
pyftsubset inter-latin.woff2 \
--output-file=inter-latin.woff2 \
--flavor=woff2 \
--layout-features='kern,liga,clig,calt' \
--unicodes="U+0000-017F,U+2000-206F,U+20AC"
Výsledné veľkosti:
| Font | Pred (Google) | Po (subset) | Úspora |
|---|---|---|---|
| Inter | 230 KB | 45 KB | –80% |
| JetBrains Mono | 56 KB | 32 KB | –43% |
| Spolu | 286 KB | 77 KB | –73% |
V global.css som pridal @font-face deklarácie:
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url('/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-017F, U+2000-206F, U+20AC;
}
A v BaseLayout.astro nahradil všetky Google Fonts linky jednoduchou preload deklaráciou:
<!-- Pred: 4 linky na externé servery -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?..." onload="..." />
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?..." /></noscript>
<!-- Po: 2 preload linky na vlastný server -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter-latin.woff2" crossorigin />
<link rel="preload" as="font" type="font/woff2" href="/fonts/jetbrains-mono-latin.woff2" crossorigin />
font-display: swap zobrazí text okamžite s fallback fontom (system-ui) a prepne na Inter/JetBrains Mono po načítaní. Používateľ vidí obsah hneď — font sa vymení bez viditeľného bliknutia.
2. Farebný kontrast (WCAG)
Problém
Lighthouse označil viacero textových elementov, kde farba textu nemala dostatočný kontrast voči pozadiu. WCAG AA vyžaduje minimálne 4.5:1 pre normálny text a 3:1 pre veľký text.
Prvá iterácia opravila najhoršie prípady, ale Lighthouse CLI test odhalil, že zinc-500 (#71717a) na tmavom pozadí stále nedosahuje 4.5:1. Rovnako zinc-400 (#a1a1aa) na bielom pozadí má len 2.56:1.
Kontrastné pomery Tailwind zinc škály
| Farba | Hex | vs biela | vs tmavé bg (~#0f1319) |
|---|---|---|---|
| zinc-300 | #d4d4d8 | 1.48:1 | — |
| zinc-400 | #a1a1aa | 2.56:1 | 5.63:1 |
| zinc-500 | #71717a | 4.83:1 | 3.97:1 |
| zinc-600 | #52525b | 7.73:1 | — |
Riešenie
Správny vzorec pre sekundárny text: text-zinc-500 dark:text-zinc-400 — obe hodnoty spĺňajú 4.5:1 vo svojom režime.
| Element | Pred | Po |
|---|---|---|
| Stats labels | text-zinc-400 dark:text-zinc-600 | text-zinc-500 dark:text-zinc-400 |
| Nav card labels | text-zinc-400 dark:text-zinc-600 | text-zinc-500 dark:text-zinc-400 |
| ”open →” text | text-zinc-300 dark:text-zinc-700 | text-zinc-500 dark:text-zinc-400 |
| Footer text + linky | text-zinc-400 dark:text-zinc-600 | text-zinc-500 dark:text-zinc-400 |
| Terminal title bar | text-zinc-400 dark:text-zinc-600 | text-zinc-500 dark:text-zinc-400 |
| CV section headers | text-zinc-400 dark:text-zinc-500 | text-zinc-500 dark:text-zinc-400 |
| Blog tag counts | text-zinc-400 dark:text-zinc-500 | text-zinc-500 dark:text-zinc-400 |
Pre terminálový výstup na homepage som nahradil inline color:#64748b (slate-500, kontrast 3.91:1 na tmavom) za CSS class s dark/light prepínaním:
/* dark mode default */
.term-out { color: #94a3b8; } /* slate-400 — 7.26:1 na tmavom */
/* light mode override */
:root:not(.dark) .term-out { color: #475569; } /* slate-600 — 7.58:1 na bielom */
Celkovo opravených 12 súborov — obidve homepage, Header, Footer, oba CV, blog listing SK/EN, blog tags SK/EN.
3. Trailing slash redirect
Problém
GitHub Pages defaultne redirectuje /en na /en/ cez 301 redirect. PageSpeed to zaznamenal ako ~925 ms zbytočnej latencie — prehliadač musí urobiť extra round-trip na server.
Riešenie
Nastavil som trailingSlash: 'always' v Astro konfigurácii:
// astro.config.mjs
export default defineConfig({
site: 'https://rebjak.com',
trailingSlash: 'always',
// ...
});
A aktualizoval som všetky interné linky v projekte, aby mali trailing slash:
<!-- Pred -->
<a href="/en/blog">Blog</a>
<a href="/cv">CV</a>
<!-- Po -->
<a href="/en/blog/">Blog</a>
<a href="/cv/">CV</a>
To isté pre dynamické linky:
<!-- Pred -->
<a href={`/blog/tag/${tag}`}>#{tag}</a>
<!-- Po -->
<a href={`/blog/tag/${tag}/`}>#{tag}</a>
Celkovo som opravil linky v 12 súboroch — homepage, blog listy, tag stránky, slug stránky a navigáciu v Header komponente.
Výsledok
Ako otestovať lokálne
# build + serve
npx astro build && npx serve dist -l 4444
# v druhom termináli
npx lighthouse http://localhost:4444/en/ \
--chrome-flags="--headless=new" \
--output=html \
--output-path=./lighthouse-report.html
Lokálne (localhost)
| Stránka | Performance | Accessibility | Best Practices | SEO |
|---|---|---|---|---|
/ (SK) | 100 | 100 | 100 | 100 |
/en/ (EN) | 100 | 100 | 100 | 100 |
/blog/ | 100 | 100 | 100 | 100 |
/en/blog/ | 100 | 100 | 100 | 100 |
Produkcia (GitHub Pages)
Lokálne 100 nie je celý príbeh. Lighthouse na produkčnom serveri testuje s reálnou sieťovou latenciou — a mobile preset simuluje pomalé 4G (1.6 Mbps, 150 ms RTT).
Pred optimalizáciou:
| Preset | Performance | Accessibility | Best Practices | SEO |
|---|---|---|---|---|
| Desktop | 97 | 94 | 100 | 100 |
| Mobile | 87 | 94 | 100 | 100 |
Po všetkých optimalizáciách:
| Preset | Performance | Accessibility | Best Practices | SEO |
|---|---|---|---|---|
| Desktop | 100 | 100 | 100 | 100 |
| Mobile | 100 | 100 | 100 | 100 |
100/100/100/100 na desktop aj mobile. Performance skočil z 87 na 100 — FCP klesol z 3.0 s na 1.1 s, LCP z 3.0 s na 2.1 s. Accessibility z 94 na 100.
Metriky pred a po (mobile)
Lighthouse mobile preset simuluje reálne podmienky — pomalé 4G (1.6 Mbps, 150 ms RTT) na zariadení Moto G Power. Podľa Google Think with Google 53% mobilných návštevníkov opustí stránku, ak sa načítava viac ako 3 sekundy.
| Metrika | Pred | Po | Zmena |
|---|---|---|---|
| First Contentful Paint | 3.0 s | 1.1 s | –63% |
| Largest Contentful Paint | 3.0 s | 2.1 s | –30% |
| Speed Index | 4.4 s | 2.6 s | –41% |
| Total Blocking Time | 0 ms | 0 ms | — |
| Cumulative Layout Shift | 0 | 0 | — |
FCP klesol na tretinu — používateľ vidí obsah takmer okamžite. LCP z 3.0 s na 2.1 s znamená, že hlavný obsah je kompletne vykreslený za ~2 sekundy. TBT 0 a CLS 0 — stránka je okamžite funkčná a nič neskáče.
Prehľad optimalizácií
| Optimalizácia | Dopad | Stav |
|---|---|---|
| Self-hosting fontov | Eliminuje 3 externé requesty | Hotové |
| Font subsetting | –73% veľkosť fontov (286 → 77 KB) | Hotové |
| Trailing slash fix | Eliminuje 301 redirect (~925 ms) | Hotové |
| WCAG kontrast (dark + light) | 100% Accessibility | Hotové |
| Terminal farby cez CSS triedy | WCAG AA v oboch režimoch | Hotové |
| Preconnect na Umami API | Ušetrí ~270 ms LCP | Hotové |
Limity GitHub Pages — na čo si dať pozor
Aj keď sa podarilo dosiahnuť 100, GitHub Pages má tvrdé limity, ktoré môžu ovplyvniť väčšie weby:
Cache hlavičky — GitHub Pages nastavuje Cache-Control: max-age=600 (10 minút) pre všetky statické assety. Aj pre súbory s hash v názve (napr. _page_.DYzwY8gP.css), ktoré by ideálne mali max-age=31536000 (1 rok) s immutable flagom. Na toto nemáte vplyv — GitHub Pages nepodporuje vlastné cache hlavičky.
Žiadne edge caching — obsah sa servuje z jedného regiónu. CDN ako Cloudflare alebo Vercel majú edge nodes po celom svete a servujú z najbližšieho servera.
Žiadna kompresia kontrola — nemôžete nastaviť Brotli kompresiu namiesto gzip, ani optimalizovať response hlavičky.
Pri malom statickom webe bez veľkých obrázkov sa dá dosiahnuť 100/100/100/100 aj na GitHub Pages. Pri väčšom webe s viacerými assetmi by tieto limity mohli stáť body — vtedy stojí za zváženie CDN ako Cloudflare alebo migrácia na Vercel/Netlify.
Čo ďalej?
- Breadcrumb schema pre blog posty
- Blog post series schema (isPartOf)
- Lazy loading pre obrázky pod foldom