Wprowadzenie: czcionki — cichy zabójca wydajności stron
W poprzednich artykułach z serii Web Perf Clinic omawialiśmy już Core Web Vitals 2026 i ich progi, optymalizację bundli JavaScript, strategie cachowania z HTTP Cache, Service Workers i Speculation Rules API, a także optymalizację obrazów z formatami AVIF, WebP i atrybutem fetchpriority. Dziś bierzemy się za temat, który — nie wiem czemu — zaskakująco często leci pod radar: czcionki webowe.
Według danych HTTP Archive z początku 2026 roku, mediana stron internetowych ładuje 5-8 plików czcionek webowych, co przekłada się na 300-500 KB nieskompresowanych danych. Żeby było jasne — to więcej niż średni rozmiar hero image'a na wielu stronach. I to nawet nie jest najgorsze. Czcionki mają jedną paskudną właściwość, której obrazy nie mają: potrafią blokować renderowanie tekstu, wywoływać FOIT (Flash of Invisible Text) i FOUT (Flash of Unstyled Text), a przy zamianie czcionek generować przesunięcia layoutu, które bezpośrednio uderzają w wyniki CLS.
Zdarzyło Ci się wejść na stronę, zobaczyć puste miejsca zamiast tekstu, a po chwili patrzeć, jak cały layout „skacze", gdy czcionka się załaduje? No właśnie. To efekt nieoptymalizowanych czcionek webowych i widuję to nagminnie — nawet na stronach, które skądinąd wyglądają na dobrze zrobione. W tym artykule pokażę konkretne techniki, które pozwolą Ci wyeliminować te problemy — od font-display i deskryptorów metryk czcionek, przez variable fonts i subsetting, aż po kompletną strategię produkcyjną. Każda technika będzie poparta działającym kodem.
Jak czcionki wpływają na Core Web Vitals
Zanim przejdziemy do rozwiązań, warto dokładnie zrozumieć, w jaki sposób czcionki webowe wpływają na poszczególne metryki wydajności. Bez tego łatwo wpaść w pułapkę optymalizowania na oślep — a tego chcemy uniknąć.
Wpływ na LCP — blokowanie renderowania tekstu
Largest Contentful Paint (LCP) mierzy czas, po którym użytkownik widzi największy element w widocznym obszarze strony. Na wielu stronach — szczególnie blogach, serwisach informacyjnych czy landing page'ach — tym największym elementem jest blok tekstu, nie obraz. I tu zaczyna się problem, którego wielu deweloperów w ogóle nie bierze pod uwagę.
Kiedy przeglądarka napotyka w CSS deklarację @font-face wskazującą na czcionkę z zewnętrznego serwera (np. Google Fonts), musi wykonać cały łańcuch operacji zanim w ogóle wyświetli tekst:
- DNS lookup — rozwiązanie nazwy domeny (np.
fonts.gstatic.com) — 20-120 ms - Nawiązanie połączenia TCP — handshake z serwerem — 30-100 ms
- Negocjacja TLS — szyfrowanie HTTPS — 30-100 ms
- Pobranie pliku CSS z listą czcionek — 20-50 ms
- Pobranie pliku czcionki (WOFF2) — 50-300 ms
Łączna suma tych opóźnień może wynieść 150-670 ms, w trakcie których tekst jest albo niewidoczny (FOIT), albo wyświetlany czcionką systemową (FOUT). Jeśli tekst jest elementem LCP — a na stronach tekstowych prawie zawsze jest — to cały ten czas dodaje się bezpośrednio do wyniku LCP. Na wolniejszych połączeniach mobilnych? Jeszcze gorzej. Opóźnienia spokojnie przekraczają sekundę.
Wpływ na CLS — przesunięcia layoutu przy zamianie czcionek
Cumulative Layout Shift (CLS) mierzy wizualną stabilność strony. I szczerze powiedziawszy, to właśnie w kontekście CLS czcionki webowe bywają najbardziej podstępne.
Problem jest taki: czcionka systemowa (fallback) i czcionka webowa mają różne metryki typograficzne — inną wysokość linii, inne proporcje znaków (ascent, descent), inny odstęp między wierszami. Kiedy przeglądarka zamienia fallback na docelową czcionkę webową, tekst zmienia swoje wymiary. Linie mogą być wyższe lub niższe, szersze lub węższe. Efekt? Wszystkie elementy poniżej tekstu się przesuwają, generując layout shift. Widziałem to dziesiątki razy na audytach i za każdym razem klient jest zaskoczony, że to czcionki są winne.
Dwa zjawiska, które to ilustrują:
- FOIT (Flash of Invisible Text) — przeglądarka ukrywa tekst do czasu załadowania czcionki. Przez 0-3 sekundy użytkownik widzi puste miejsce, a potem tekst pojawia się nagle. Technicznie nie generuje to CLS bezpośrednio (bo element ma zarezerwowane miejsce), ale drastycznie pogarsza LCP i ogólne doświadczenie użytkownika. Po prostu strona wygląda na zepsutą.
- FOUT (Flash of Unstyled Text) — przeglądarka wyświetla tekst czcionką systemową, a po załadowaniu zamienia na czcionkę webową. To już generuje CLS, ponieważ wymiary tekstu się zmieniają. W typowych przypadkach taka zamiana powoduje przesunięcia layoutu o wartości 0.05-0.15, co potrafi zjeść sporą część budżetu CLS (próg „dobrego" CLS to 0.1).
Wpływ na FCP i TBT
First Contentful Paint (FCP) oznacza moment, w którym przeglądarka renderuje pierwszy element wizualny. Jeśli arkusz CSS z deklaracjami czcionek jest ładowany z zewnętrznego serwera, jego parsowanie blokuje renderowanie — FCP jest opóźniony o czas potrzebny na pobranie i przetworzenie tego CSS.
Total Blocking Time (TBT) też może ucierpieć, choć w mniejszym stopniu. Parsowanie dużych plików CSS z wieloma deklaracjami @font-face (np. Google Fonts CSS z wieloma wariantami) zajmuje czas w głównym wątku. Nie jest to zazwyczaj główny winowajca TBT, ale w połączeniu z innymi problemami potrafi przechylić szalę — szczególnie na słabszych urządzeniach mobilnych.
font-display: swap vs optional — który wybrać?
Właściwość CSS font-display w deklaracji @font-face kontroluje zachowanie przeglądarki podczas ładowania czcionki. To jedno z najważniejszych narzędzi w arsenale optymalizacji czcionek, a jednocześnie jedno z najczęściej źle rozumianych (poważnie — ilość projektów, w których widzę font-display: block albo brak jakiejkolwiek deklaracji, jest zatrważająca). Przejdźmy przez wszystkie wartości:
auto— zachowanie domyślne przeglądarki (zwykle równoważneblock). Tekst jest niewidoczny przez ~3 sekundy, potem pokazywany czcionką fallback. Nie polecam — oddajemy kontrolę przeglądarce, a to rzadko kończy się dobrze.block— tekst jest niewidoczny przez krótki okres (zazwyczaj 3 sekundy), w tym czasie przeglądarka czeka na czcionkę. Jeśli się nie załaduje — wyświetla fallback. Generuje FOIT i pogarsza LCP. Praktycznie nigdy nie jest dobrym wyborem.swap— tekst jest natychmiast wyświetlany czcionką fallback. Gdy czcionka webowa się załaduje, przeglądarka ją podmienia. Generuje FOUT i potencjalnie CLS, ale tekst jest widoczny od razu — dobre dla LCP.fallback— kompromis. Tekst jest niewidoczny przez bardzo krótki okres (~100 ms), potem wyświetlany czcionką fallback. Czcionka webowa zostanie podmieniona tylko jeśli załaduje się w ciągu ~3 sekund. Minimalizuje zarówno FOIT, jak i FOUT.optional— najbardziej agresywna opcja wydajnościowa i moja ulubiona tam, gdzie mogę sobie na nią pozwolić. Tekst jest niewidoczny przez ultra-krótki okres (~100 ms). Jeśli czcionka nie załaduje się w tym czasie, przeglądarka używa fallbacka i nie zamienia czcionki nawet po jej załadowaniu. Czcionka zostanie użyta dopiero przy kolejnej nawigacji (z cache). Zero CLS z zamiany czcionek.
Oto przykłady implementacji:
/* font-display: swap — dla czcionek kluczowych dla marki */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* font-display: optional — dla maksymalnej wydajności */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: optional;
}
Kiedy wybrać którą wartość? Oto moja rekomendacja na podstawie lat pracy z różnymi projektami:
swap— gdy czcionka jest kluczowa dla rozpoznawalności marki i absolutnie musi się wyświetlić (np. logo, nagłówki w specyficznym kroju). Połącz z deskryptorami metryk (o których za chwilę), żeby zminimalizować CLS.optional— gdy priorytetem jest wydajność i stabilność layoutu. Idealne dla body text, gdzie czcionka systemowa jest akceptowalnym zamiennikiem. Najlepszy wybór pod kątem CLS — gwarantuje zero przesunięć z zamiany czcionek.fallback— dobry kompromis, gdy chcesz, żeby czcionka się wyświetliła, ale nie za wszelką cenę. Na szybkich połączeniach zachowa się jakswap, na wolnych — jakoptional.
Moja ogólna rada na 2026 rok? Używaj font-display: optional dla tekstu treści i font-display: swap z deskryptorami metryk dla nagłówków i elementów brandingowych. Testowałem to podejście na kilkunastu projektach — daje najlepszą równowagę między estetyką a wydajnością.
Eliminacja CLS za pomocą deskryptorów metryk czcionek
OK, jeśli zdecydowałeś się na font-display: swap (bo potrzebujesz, żeby czcionka webowa ostatecznie się wyświetliła), kluczowe staje się zminimalizowanie przesunięć layoutu przy zamianie. I tu wchodzą na scenę deskryptory metryk czcionek — moim zdaniem jedno z najbardziej niedocenianych narzędzi w całym web performance.
size-adjust, ascent-override, descent-override i line-gap-override
Te cztery deskryptory CSS pozwalają dopasować metryki czcionki fallback do metryk czcionki webowej tak, aby zamiana była praktycznie niezauważalna:
size-adjust— skaluje rozmiar glyfów czcionki fallback. Jeśli czcionka fallback jest szersza/węższa niż docelowa, ten deskryptor wyrówna proporcje. Wartość w procentach (np.size-adjust: 107%).ascent-override— nadpisuje metrykę ascent (wysokość znaków nad linią bazową). Kontroluje, ile miejsca tekst zajmuje powyżej linii bazowej.descent-override— nadpisuje metrykę descent (głębokość znaków poniżej linii bazowej, np. dolna część liter „g", „p", „y").line-gap-override— kontroluje dodatkową przestrzeń między wierszami wynikającą z metadanych czcionki (nie mylić z CSSline-height).
Jak to działa w praktyce? Definiujemy dodatkową deklarację @font-face dla czcionki fallback, nadając jej takie metryki, które odpowiadają czcionce docelowej. Brzmi skomplikowanie, ale w gruncie rzeczy to kilka linijek CSS. Oto kompletny przykład dopasowania czcionki systemowej Arial do Inter:
/* Czcionka docelowa — Inter Variable */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* Czcionka fallback z dopasowanymi metrykami do Inter */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.64%;
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
}
/* Użycie w CSS — fallback jako drugi w font-family */
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
/* Alternatywnie dla systemów macOS/iOS */
@font-face {
font-family: 'Inter Fallback Mac';
src: local('Helvetica Neue'), local('Helvetica');
size-adjust: 106.57%;
ascent-override: 91.39%;
descent-override: 22.78%;
line-gap-override: 0%;
}
Dzięki temu, gdy przeglądarka wyświetla tekst czcionką „Inter Fallback" (czyli tak naprawdę Arial ze zmodyfikowanymi metrykami), a potem zamienia na Inter, różnica w wymiarach jest minimalna. Przesunięcia layoutu spadają z typowych 0.05-0.15 do praktycznie 0. Pierwszy raz, gdy to zobaczyłem w akcji na realnym projekcie, byłem szczerze pod wrażeniem — zamiana czcionki była praktycznie niewidoczna.
Jak obliczyć te wartości? Teoretycznie możesz to zrobić ręcznie, porównując metryki obu czcionek (ascent, descent, line gap, units per em) z tabel czcionek OpenType. Ale — bądźmy szczerzy — nikt tego nie robi ręcznie. Użyj do tego narzędzi.
Narzędzia do automatycznego generowania metryk
Na szczęście w 2026 roku istnieje kilka świetnych narzędzi, które zrobią to za Ciebie:
- Fontaine — plugin do Vite i Webpack, który automatycznie generuje deklaracje
@font-facez dopasowanymi metrykami. Po zainstalowaniu (npm install fontaine) wystarczy dodać go do konfiguracji bundlera, a resztę zrobi za Ciebie. Analizuje pliki czcionek w projekcie i generuje odpowiednie override'y. Proste i skuteczne. - next/font — jeśli używasz Next.js, wbudowany moduł
next/fontautomatycznie obliczasize-adjusti pozostałe deskryptory dla czcionek Google Fonts i lokalnych. To jest najlepsze rozwiązanie „z pudełka" — zero konfiguracji, automatyczny self-hosting i dopasowanie metryk. Serio, jeśli jesteś na Next.js i tego nie używasz, to tracisz darmowe punkty wydajności. - Fallback Font Generator (screenspan.net/font-style) — interaktywne narzędzie online. Wgrywasz plik czcionki webowej, wybierasz czcionkę fallback, a narzędzie wizualnie porównuje obie i generuje gotowe CSS z deskryptorami. Świetne do jednorazowego użycia albo do szybkiego prototypowania.
- Font Style Matcher (meowni.ca/font-style-matcher) — podobne narzędzie online do wizualnego porównywania i dopasowywania metryk czcionek.
Przykład konfiguracji Fontaine z Vite:
// vite.config.js
import { defineConfig } from 'vite';
import { FontaineTransform } from 'fontaine';
export default defineConfig({
plugins: [
FontaineTransform.vite({
fallbacks: ['Arial', 'Helvetica Neue'],
resolvePath: (id) => new URL(`./public${id}`, import.meta.url),
}),
],
});
Variable fonts — jeden plik zamiast dziesięciu
Jednym z najskuteczniejszych sposobów optymalizacji czcionek webowych jest przejście na czcionki zmienne (variable fonts). Jeśli jeszcze z nich nie korzystasz — ta sekcja powinna Cię przekonać.
Czym są czcionki zmienne?
Tradycyjnie każda kombinacja grubości (weight) i stylu (italic) czcionki wymagała osobnego pliku. Chcesz Regular, Medium, Bold i Black? Cztery pliki. Dodaj italic do każdego? Osiem plików. Robi się tego sporo.
Czcionki zmienne (variable fonts) rozwiązują ten problem w elegancki sposób — umieszczają wiele wariantów w jednym pliku.
Zamiast dyskretnych grubości (400, 500, 700, 900), czcionka zmienna definiuje osie wariacji (variation axes), po których można płynnie przechodzić. Najważniejsza oś to wght (weight), która pozwala ustawić dowolną grubość w zakresie 100-900 — nie tylko 400 czy 700, ale także 450, 523 czy 812 (gdybyś z jakiegoś powodu potrzebował akurat takiej). Inne popularne osie to:
- wdth (width) — szerokość znaków (condensed/expanded)
- ital (italic) — kursywa
- slnt (slant) — nachylenie
- opsz (optical size) — optymalizacja kształtów dla różnych rozmiarów
Oszczędności w praktyce
Weźmy najpopularniejszą czcionkę w sieci — Inter. Porównajmy tradycyjne podejście z variable fonts:
Tradycyjne podejście — 4 osobne pliki WOFF2:
- Inter-Regular.woff2 — ~80 KB
- Inter-Medium.woff2 — ~82 KB
- Inter-Bold.woff2 — ~82 KB
- Inter-Black.woff2 — ~78 KB
- Łącznie: ~322 KB, 4 żądania HTTP
Czcionka zmienna — 1 plik WOFF2:
- Inter-var-latin.woff2 — ~110 KB
- Łącznie: ~110 KB, 1 żądanie HTTP
To redukcja o ~66% rozmiaru i z 4 żądań HTTP do jednego. Na urządzeniach mobilnych, gdzie każde żądanie HTTP ma znacznie wyższe opóźnienie, jedno żądanie zamiast czterech potrafi zaoszczędzić 200-400 ms czasu ładowania. A jeśli używasz więcej wariantów (np. 6-8), oszczędności robią się naprawdę imponujące.
Implementacja w CSS
Deklaracja @font-face dla czcionki zmiennej wygląda nieco inaczej niż dla tradycyjnej:
/* Deklaracja czcionki zmiennej z zakresem grubości */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin.woff2') format('woff2');
font-weight: 100 900; /* Zakres, nie pojedyncza wartość! */
font-style: normal;
font-display: swap;
}
/* Osobna deklaracja dla italic (jeśli potrzebna) */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin-italic.woff2') format('woff2');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}
/* Użycie standardowych wartości font-weight */
h1 { font-weight: 800; }
h2 { font-weight: 700; }
p { font-weight: 400; }
/* Użycie niestandardowych wartości — to działa tylko z variable fonts! */
.subtitle { font-weight: 350; }
.semi-bold { font-weight: 550; }
/* Zaawansowane — bezpośredni dostęp do osi wariacji */
.custom-text {
font-variation-settings: 'wght' 625, 'wdth' 87;
}
/* Animacja grubości czcionki — płynna i wydajna */
.animated-weight {
font-weight: 400;
transition: font-weight 0.3s ease;
}
.animated-weight:hover {
font-weight: 700;
}
Wsparcie przeglądarek dla variable fonts w 2026 roku jest praktycznie pełne — Chrome, Firefox, Safari, Edge, a nawet przeglądarki mobilne obsługują je od lat. Według caniuse.com, wsparcie globalne przekracza 97%. Nie ma więc żadnego powodu, by z nich nie korzystać. Naprawdę — jeśli tego jeszcze nie robisz, to najwyższy czas.
Self-hosting vs Google Fonts — dlaczego hostować lokalnie?
Google Fonts to najpopularniejszy serwis do czcionek webowych — używa go ponad 60% stron korzystających z czcionek webowych. Jest wygodny, darmowy i prosty w integracji. Ale w 2026 roku self-hosting jest niemal zawsze lepszym wyborem. I mówię to jako ktoś, kto sam przez lata bezmyślnie wklejał linki do Google Fonts w każdym projekcie. Oto dlaczego zmieniłem zdanie:
- Prywatność i GDPR — ładowanie czcionek z
fonts.googleapis.comoznacza, że adres IP każdego odwiedzającego jest wysyłany do Google. W 2022 roku niemiecki sąd orzekł, że to naruszenie GDPR i nałożył grzywnę na stronę korzystającą z Google Fonts bez zgody użytkownika. W 2026 roku świadomość tych problemów jest jeszcze większa, a regulacje surowsze. - Wydajność — self-hosting eliminuje potrzebę DNS lookup do
fonts.googleapis.comifonts.gstatic.com, nawiązywania osobnych połączeń TCP/TLS i pobierania dodatkowego pliku CSS. Czcionki są serwowane z tej samej domeny co strona, korzystając z już nawiązanego połączenia. To oszczędność 100-300 ms. Niby niewiele, ale na mobilnych to potrafi zrobić różnicę między „dobrym" a „wymaga poprawy" w LCP. - Kontrola nad cachingiem — Google Fonts ustawia cache na maksymalnie 1 rok, ale nie masz kontroli nad nagłówkami. Przy self-hostingu możesz ustawić
Cache-Control: public, max-age=31536000, immutablei korzystać z nazw plików z hashem (np.inter-var-latin.a3b8f2.woff2) dla idealnego cache busting. - Kontrola nad subsettingiem — Google Fonts automatycznie dzieli czcionki na subset'y, ale nie masz kontroli nad tym procesem. Self-hosting pozwala stworzyć dokładnie taki subset, jakiego potrzebujesz — z polskimi znakami, ale bez cyrylicy czy greki.
- Brak zależności od zewnętrznego serwisu — jeśli Google Fonts ma problemy (rzadko, ale się zdarza) lub jest blokowany (np. w Chinach), Twoja strona nie ucierpi.
Jak przeprowadzić migrację z Google Fonts na self-hosting? Oto krok po kroku:
<!-- KROK 1: BYŁO — ładowanie z Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<!-- KROK 2: Pobierz pliki czcionek -->
<!-- Użyj https://gwfh.mranftl.com/fonts (google-webfonts-helper) -->
<!-- lub pobierz bezpośrednio z repozytorium czcionki na GitHubie -->
<!-- Umieść pliki WOFF2 w katalogu /fonts/ na serwerze -->
<!-- KROK 3: JEST — lokalne @font-face + preload -->
<link rel="preload" href="/fonts/inter-var-latin.woff2"
as="font" type="font/woff2" crossorigin>
/* KROK 3 (ciąg dalszy): Deklaracja @font-face w CSS */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0100-024F, U+0259, U+1E00-1EFF,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
}
Cały proces zajmuje 15-30 minut, a korzyści są trwałe. Narzędzie google-webfonts-helper (gwfh.mranftl.com) automatycznie generuje pliki WOFF2 i odpowiednie deklaracje CSS dla wybranej czcionki — polecam zacząć od niego. Jak raz to zrobisz, nie wrócisz do Google Fonts.
Subsetting — ładuj tylko potrzebne znaki
Pełny plik czcionki zawiera glyfy dla wielu alfabetów: łaciński, cyrylica, greka, wietnamski, a nawet arabski. Jeśli Twoja strona jest w języku polskim, potrzebujesz tylko podstawowego zestawu łacińskiego plus polskie znaki diakrytyczne. Reszta to martwy ciężar. Po co go wozić?
Czym jest subsetting?
Subsetting to proces usuwania niepotrzebnych glyfów z pliku czcionki. Efekty bywają wręcz spektakularne — przykładowo, pełna czcionka Inter Regular w formacie WOFF2 to ~95 KB. Po subsettingu do znaków łacińskich z polskimi rozszerzeniami: ~32 KB. To redukcja o ponad 66%.
Jeśli na stronie używasz 2-3 czcionek, subsetting może zaoszczędzić 100-200 KB transferu. Na połączeniach mobilnych to jest naprawdę istotna różnica.
pyftsubset w praktyce
Najpopularniejszym narzędziem do subsettingu jest pyftsubset z pakietu fonttools. Oto jak go użyć:
# Instalacja
pip install fonttools brotli
# Subsetting do łacińskiego z polskimi znakami
# --flavor=woff2 generuje format WOFF2
# --layout-features zachowuje OpenType features (kerning, ligatury)
pyftsubset Inter-VariableFont_opsz,wght.ttf \
--output-file=inter-var-latin-pl.woff2 \
--flavor=woff2 \
--layout-features='kern,liga,clig,calt,mark,mkmk' \
--unicodes='U+0000-00FF,U+0100-017F,U+0180-024F,U+0259,U+1E00-1EFF,U+2000-206F,U+2070-209F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD'
# Zakresy Unicode wyjaśnienie:
# U+0000-00FF — Basic Latin (ASCII + Latin-1 Supplement)
# U+0100-017F — Latin Extended-A (zawiera ą, ć, ę, ł, ń, ó, ś, ź, ż)
# U+0180-024F — Latin Extended-B
# U+2000-206F — General Punctuation (em dash, cudzysłowy, itp.)
# U+20AC — znak euro (€)
# Sprawdzenie rozmiaru
ls -lh inter-var-latin-pl.woff2
Jeśli chcesz jeszcze agresywniejszy subset — tylko znaki faktycznie użyte na stronie — możesz wyeksportować listę znaków z narzędzia glyphhanger:
# Instalacja glyphhanger
npm install -g glyphhanger
# Analiza znaków używanych na stronie
glyphhanger https://twoja-strona.pl --subset=Inter-VariableFont.ttf --formats=woff2
unicode-range w CSS
Deklaracja unicode-range w @font-face mówi przeglądarce, jakie zakresy znaków obsługuje dany plik czcionki. Dzięki temu przeglądarka pobiera plik tylko wtedy, gdy strona faktycznie zawiera znaki z tego zakresu. To w zasadzie ta sama technika, którą stosuje Google Fonts — dzieli czcionkę na wiele małych plików (dla łacińskiego, cyrylicy, greki itd.) i ładuje tylko potrzebne.
Nic nie stoi na przeszkodzie, żeby zastosować to samo podejście przy self-hostingu:
/* Subset łaciński — podstawowe znaki ASCII */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin-basic.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+2000-206F, U+20AC, U+2122;
}
/* Subset łaciński rozszerzony — polskie i inne znaki CE */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin-ext.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF;
}
/* Subset cyrylica — tylko jeśli strona ma treść w cyrylicy */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-cyrillic.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
W przypadku strony w języku polskim przeglądarka pobierze subset łaciński i łaciński rozszerzony (bo polskie znaki ą, ć, ę itd. są w zakresie U+0100-017F), ale nie pobierze subset cyrylicy — oszczędzając dodatkowe kilka-kilkanaście KB. Mały zysk? Może. Ale takie małe zyski się sumują.
Preload i kolejność ładowania czcionek
Nawet najlepiej zoptymalizowana czcionka nie pomoże, jeśli przeglądarka odkryje ją za późno. Standardowy proces wygląda tak: przeglądarka pobiera HTML, parsuje CSS, odkrywa @font-face, dopiero wtedy zaczyna pobierać czcionkę. To oznacza, że pobieranie czcionki nie rozpocznie się, dopóki CSS nie zostanie w pełni sparsowany — a to może być za późno. Dyrektywa <link rel="preload"> pozwala przeskoczyć ten łańcuch i rozpocząć pobieranie natychmiast.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moja strona</title>
<!-- Preload TYLKO 1-2 najważniejszych czcionek!
Więcej preloadów = mniejsza skuteczność każdego z nich -->
<link rel="preload"
href="/fonts/inter-var-latin.woff2"
as="font"
type="font/woff2"
crossorigin
fetchpriority="high">
<!-- UWAGA: atrybut crossorigin jest OBOWIĄZKOWY!
Bez niego przeglądarka pobierze czcionkę dwukrotnie:
raz z preload (bez CORS) i raz z @font-face (z CORS).
Wartość "anonymous" jest domyślna, więc samo "crossorigin" wystarczy. -->
<!-- CSS ze zdefiniowanymi @font-face -->
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/main.css">
</head>
Kilka kluczowych zasad dotyczących preload czcionek (wyciągniętych z własnych, czasem bolesnych doświadczeń):
- Preloaduj maksymalnie 1-2 czcionki — każdy preload konkuruje o przepustowość z innymi zasobami. Preloadowanie 5 czcionek sprawi, że wszystkie załadują się wolniej, niż gdyby żadna nie była preloadowana. Widziałem to w praktyce i efekt jest dokładnie odwrotny od zamierzonego.
- Zawsze dodawaj
crossorigin— czcionki w@font-facesą zawsze pobierane w trybie CORS (nawet z tej samej domeny!). Bez atrybutucrossoriginna preload, przeglądarka nie dopasuje preloadowanego zasobu do żądania z CSS i pobierze czcionkę dwa razy. To klasyczny błąd, który łatwo przeoczyć. - Używaj
type="font/woff2"— to pozwala przeglądarkom, które nie obsługują WOFF2 (w 2026 roku praktycznie żadna), pominąć preload zamiast pobierać niepotrzebny plik. - Rozważ
fetchpriority="high"— jak omawialiśmy w artykule o optymalizacji obrazów, atrybutfetchprioritypozwala jawnie określić priorytet pobierania. Dla czcionki używanej w tekście powyżej linii zgięcia, wysokie priorytety mają sens. - Preloaduj konkretny plik, nie CSS — preloadowanie pliku CSS z Google Fonts (
fonts.googleapis.com/css2?...) jest mniej efektywne niż preloadowanie bezpośrednio pliku WOFF2, bo przeglądarka i tak musi potem sparsować CSS i znaleźć URL czcionki.
Kompletna strategia optymalizacji — krok po kroku
Czas połączyć wszystkie techniki w spójną strategię. Oto 7 kroków, które pozwolą Ci zoptymalizować czcionki webowe od podstaw. Kolejność nie jest przypadkowa — każdy krok buduje na poprzednim:
- Audyt obecnych czcionek — otwórz DevTools, zakładka Network, filtr „Font". Sprawdź, ile plików czcionek się ładuje, skąd, ile ważą i jak długo trwa ich pobranie. Zidentyfikuj czcionki, które są ładowane, ale nie używane. (Zdziwisz się, jak często takie zombie-czcionki się trafiają.)
- Ogranicz liczbę czcionek do minimum — czy naprawdę potrzebujesz trzech różnych krojów? W większości przypadków wystarczą jedna czcionka do tekstu i opcjonalnie jedna do nagłówków. Porozmawiaj z designerem — zwykle da się to wynegocjować.
- Przejdź na variable fonts — zastąp wiele plików jednym plikiem czcionki zmiennej. Oszczędzisz 50-70% rozmiaru i zredukujesz liczbę żądań HTTP.
- Zastosuj subsetting — użyj
pyftsubset, aby usunąć niepotrzebne glyfy. Dla polskich stron: zachowaj Basic Latin + Latin Extended-A/B. - Self-hostuj czcionki — przenieś pliki WOFF2 na swój serwer. Skonfiguruj odpowiednie nagłówki cachowania.
- Skonfiguruj font-display i deskryptory metryk — użyj
font-display: optionallubswap+ deskryptory metryk dla eliminacji CLS. - Dodaj preload dla krytycznej czcionki — preloaduj 1 najważniejszą czcionkę z atrybutem
crossorigin.
Oto kompletny, produkcyjny kod łączący wszystkie techniki:
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zoptymalizowana strona</title>
<!-- Preload jednej najważniejszej czcionki -->
<link rel="preload"
href="/fonts/inter-var-latin-pl.woff2"
as="font"
type="font/woff2"
crossorigin
fetchpriority="high">
<style>
/* === CZCIONKI === */
/* Czcionka główna — Inter Variable (self-hosted, subset) */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin-pl.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0100-024F, U+1E00-1EFF,
U+2000-206F, U+20AC, U+2122, U+FEFF, U+FFFD;
}
/* Fallback z dopasowanymi metrykami — eliminacja CLS */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.64%;
ascent-override: 90.49%;
descent-override: 22.56%;
line-gap-override: 0%;
}
/* Fallback dla macOS */
@font-face {
font-family: 'Inter Fallback Mac';
src: local('Helvetica Neue');
size-adjust: 106.57%;
ascent-override: 91.39%;
descent-override: 22.78%;
line-gap-override: 0%;
}
/* === TYPOGRAFIA === */
body {
font-family: 'Inter', 'Inter Fallback', 'Inter Fallback Mac',
-apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, sans-serif;
font-weight: 400;
line-height: 1.6;
}
h1, h2, h3, h4 {
font-weight: 700;
line-height: 1.2;
}
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
</style>
</head>
<body>
<h1>Zoptymalizowana strona z czcionkami</h1>
<p>Tekst renderowany bez FOIT, bez CLS, z minimalnym rozmiarem pliku.</p>
</body>
</html>
Oczekiwane wyniki po wdrożeniu wszystkich optymalizacji:
- Rozmiar czcionek: z ~300-500 KB do ~30-110 KB (redukcja 60-90%)
- Liczba żądań HTTP: z 5-8 do 1-2 (redukcja 75-85%)
- CLS z zamiany czcionek: z 0.05-0.15 do ~0 (eliminacja)
- LCP: poprawa o 100-500 ms (eliminacja opóźnień zewnętrznych serwerów)
- FCP: poprawa o 50-200 ms (brak blokowania przez zewnętrzny CSS)
To nie są teoretyczne liczby — to realne wyniki, które konsekwentnie widzę na projektach po wdrożeniu opisanych technik. Ostatnio na jednym e-commerce'ie samo przejście z Google Fonts na self-hosted variable font z subsettingiem dało poprawę LCP o ponad 350 ms na mobilnych. Jeśli Twoja strona ładuje 5-8 plików czcionek z Google Fonts bez subsettingu, wdrożenie tej strategii może być jedną z najbardziej opłacalnych optymalizacji, jakie zrobisz w tym roku.
Najczęściej zadawane pytania (FAQ)
Czy font-display: swap powoduje CLS?
Tak, ale da się to wyeliminować. Samo font-display: swap powoduje FOUT — przeglądarka wyświetla tekst czcionką fallback, a po załadowaniu czcionki webowej podmienia ją. Ponieważ czcionka fallback i webowa mają różne metryki (wysokość, szerokość znaków), zamiana powoduje przesunięcia layoutu, czyli CLS. Jednak jeśli zastosujesz deskryptory metryk czcionek (size-adjust, ascent-override, descent-override, line-gap-override), dopasowując fallback do czcionki docelowej, przesunięcia spadają do praktycznie zera. Alternatywnie, font-display: optional całkowicie eliminuje CLS z zamiany czcionek, bo czcionka webowa nie jest podmieniana po załadowaniu — zostaje użyta dopiero przy kolejnej nawigacji.
Ile czcionek webowych mogę bezpiecznie załadować?
Rekomendacja na 2026 rok: maksymalnie 2-3 pliki czcionek (po optymalizacji). Idealnie: jedna czcionka zmienna (variable font) obejmująca wszystkie potrzebne grubości. Jeśli potrzebujesz dwóch krojów (np. bezszeryfowy do tekstu i szeryfowy do nagłówków), to dwa pliki czcionek zmiennych. Każdy dodatkowy plik to dodatkowe żądanie HTTP, dodatkowe bajty do pobrania i dodatkowe ryzyko opóźnień. Pamiętaj też, że preloaduj maksymalnie 1-2 z nich — nie wszystkie. Kluczowa zasada, którą stosuję: jeśli jakaś czcionka jest używana na mniej niż 30% stron serwisu, poważnie zastanów się, czy w ogóle jest potrzebna jako czcionka webowa.
Czy variable fonts są obsługiwane przez wszystkie przeglądarki?
Tak, wsparcie jest praktycznie pełne. W 2026 roku czcionki zmienne są obsługiwane przez Chrome (od wersji 66), Firefox (od 62), Safari (od 11), Edge (od 17) i wszystkie główne przeglądarki mobilne. Globalne wsparcie według caniuse.com przekracza 97%. Jedyne przeglądarki bez wsparcia to naprawdę stare wersje (IE11, bardzo stare wersje Android Browser), które w 2026 roku stanowią marginalny procent ruchu. Możesz bezpiecznie używać variable fonts bez fallbacka na wiele plików — przeglądarki bez wsparcia po prostu wyświetlą tekst czcionką systemową, co jest w pełni akceptowalnym zachowaniem.
Jak sprawdzić, które czcionki spowalniają moją stronę?
Najszybsza metoda (i ta, od której zawsze zaczynam): otwórz Chrome DevTools, zakładka Network, filtr „Font". Zobaczysz listę wszystkich pobieranych czcionek, ich rozmiar, czas pobierania i źródło. Posortuj po rozmiarze lub czasie, żeby znaleźć największe problemy. Druga metoda: Chrome DevTools, zakładka Performance, nagraj sesję i szukaj zdarzeń „Layout Shift" powiązanych z załadowaniem czcionek. Trzecia metoda: uruchom Lighthouse — raport zawiera sekcję „Avoid enormous network payloads" (gdzie zobaczysz duże pliki czcionek) i „Ensure text remains visible during webfont load" (która sprawdza, czy używasz font-display). Możesz też użyć WebPageTest — wodospad (waterfall) dokładnie pokaże, kiedy czcionki są odkrywane i ile czasu zajmuje ich pobranie. Osobiście najczęściej używam kombinacji DevTools + WebPageTest — to daje najpełniejszy obraz sytuacji.
Czy Google Fonts jest złym wyborem w 2026 roku?
Google Fonts nie jest „złym" wyborem, ale self-hosting jest prawie zawsze lepszy. Google Fonts wykonał ogromną pracę: automatyczny subsetting, serwowanie optymalnego formatu (WOFF2), rozbudowana sieć CDN. Problem polega na tym, że w 2026 roku te zalety nie rekompensują wad. Po pierwsze, kwestie GDPR — adresy IP użytkowników są przesyłane do Google, co wymaga informowania o tym w polityce prywatności i potencjalnie uzyskania zgody. Po drugie, dodatkowe połączenia sieciowe — nawet z preconnect, dwa zewnętrzne żądania DNS + TCP + TLS dodają 100-300 ms opóźnienia. Po trzecie — i to jest argument, który wielu deweloperów wciąż pomija — od Chrome 86 (2020) pamięć podręczna jest partycjonowana per domena. To oznacza, że nawet jeśli użytkownik odwiedził inną stronę korzystającą z tego samego pliku z Google Fonts, Twoja strona i tak pobierze go od nowa. Dawny argument „użytkownicy mają już tę czcionkę w cache" nie jest prawdziwy od ponad 5 lat. Czas go pogrzebać. Jeśli jednak z jakiegoś powodu musisz zostać przy Google Fonts (np. wymagania projektu, ograniczenia infrastruktury), pamiętaj o <link rel="preconnect"> do obu domen i o parametrze &display=swap w URL.