rebjak.com
RSS English
← Blog

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:

MetrikaSkóre
Performance87
Accessibility94
Best Practices100
SEO100

Tri hlavné problémy:

  1. Google Fonts — render-blocking + externé requesty, ušetriteľných ~2000 ms
  2. Nedostatočný farebný kontrast — WCAG AA zlyhanie v dark aj light mode
  3. 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:

FontPred (Google)Po (subset)Úspora
Inter230 KB45 KB–80%
JetBrains Mono56 KB32 KB–43%
Spolu286 KB77 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

FarbaHexvs bielavs tmavé bg (~#0f1319)
zinc-300#d4d4d81.48:1
zinc-400#a1a1aa2.56:15.63:1
zinc-500#71717a4.83:13.97:1
zinc-600#52525b7.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.

ElementPredPo
Stats labelstext-zinc-400 dark:text-zinc-600text-zinc-500 dark:text-zinc-400
Nav card labelstext-zinc-400 dark:text-zinc-600text-zinc-500 dark:text-zinc-400
”open →” texttext-zinc-300 dark:text-zinc-700text-zinc-500 dark:text-zinc-400
Footer text + linkytext-zinc-400 dark:text-zinc-600text-zinc-500 dark:text-zinc-400
Terminal title bartext-zinc-400 dark:text-zinc-600text-zinc-500 dark:text-zinc-400
CV section headerstext-zinc-400 dark:text-zinc-500text-zinc-500 dark:text-zinc-400
Blog tag countstext-zinc-400 dark:text-zinc-500text-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ánkaPerformanceAccessibilityBest PracticesSEO
/ (SK)100100100100
/en/ (EN)100100100100
/blog/100100100100
/en/blog/100100100100

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:

PresetPerformanceAccessibilityBest PracticesSEO
Desktop9794100100
Mobile8794100100

Po všetkých optimalizáciách:

PresetPerformanceAccessibilityBest PracticesSEO
Desktop100100100100
Mobile100100100100

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.

MetrikaPredPoZmena
First Contentful Paint3.0 s1.1 s–63%
Largest Contentful Paint3.0 s2.1 s–30%
Speed Index4.4 s2.6 s–41%
Total Blocking Time0 ms0 ms
Cumulative Layout Shift00

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áciaDopadStav
Self-hosting fontovEliminuje 3 externé requestyHotové
Font subsetting–73% veľkosť fontov (286 → 77 KB)Hotové
Trailing slash fixEliminuje 301 redirect (~925 ms)Hotové
WCAG kontrast (dark + light)100% AccessibilityHotové
Terminal farby cez CSS triedyWCAG AA v oboch režimochHotové
Preconnect na Umami APIUšetrí ~270 ms LCPHotové

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