Dla administratorów: pełny przebieg z dokładnymi liczbami, poleceniami i konfiguracją. Wersje: stan na listopad 2026.
W skrócie
| Element | Szczegóły |
| Środowisko | Craft CMS |
| Cel | PSI 100 w czterech kategoriach na /, komputer i telefon; nasz budżet: wszystkie kategorie ≥ 95, LCP < 2,0 s, INP < 200 ms, CLS < 0,05 |
| Stan wyjściowy | PSI 03.11.2026: komputer 100/100/100/100 (FCP 0,3 s, LCP 0,4 s, TBT 0 ms, CLS 0, SI 0,6 s); telefon 99/100/100/100 (FCP 1,0 s, LCP 2,3 s, TBT 0 ms, CLS 0, SI 1,8 s) |
| Przyczyna 99 | element LCP na telefonie to <p class="reveal"> nad H1 (zdjęcie tła zakrywa cały viewport i nie jest kandydatem LCP), wchodzący z opacity: 0; symulacja Lantern dolicza do niego zdjęcie tła 2400w (122 KiB, fetchpriority="high"), plakat JPEG (49 KiB) i czcionki: LCP 2,25 s = wynik metryki 0,94 |
| Rozwiązanie | rewizja r740 (03.11.2026, na produkcji przed 13:05): @keyframes reveal bez opacity: 0; tło na telefonie sizes 160vw (1280w, 31 KiB) i bez fetchpriority; plakat filmu w AVIF (26 049 B zamiast 50 130 B) z preload tylko na telefonach; reguła nginx z Cache-Control dla .avif. Potem r743, r744, r747 (na produkcji przed 14:21): kadr filmu jako <img srcset> zamiast poster, preload tła tylko na komputerze, bez wymuszonych przeformatowań, chunki filmu i statystyk po load, modulepreload/preload as=style dla pliku wejściowego, kadr AVIF na „O nas” |
| Efekt | PSI 03.11.2026 13:05: komputer i telefon 100/100/100/100 (telefon: FCP 0,9 s, LCP 1,7 s, SI 1,4 s). Lighthouse 13.6.0 na produkcji 13:09–13:15: telefon 100 ×3 (LCP 1,65–1,66 s, wynik metryki 0,99; element LCP: plakat filmu), komputer 100 ×3; podstrony bez filmu 100/100 na obu profilach, /o-nas na telefonie nadal 99; efekt uboczny: na komputerze nowa diagnostyka „Wykrywanie żądań LCP”. PSI 14:21 (po r747): 100/100/100/100 na obu profilach, telefon LCP 1,5 s, jedyna podpowiedź „Żądania blokujące renderowanie”. Lighthouse 14:23–14:28: strona główna 100 ×3 na obu profilach (LCP telefon 1,50–1,51 s), wszystkie pięć podstron 100/100 na obu profilach, /o-nas telefon 100 ×3 (LCP 1,46–1,47 s) |
Kontekst
Wymagania właściciela, które z punktu widzenia wydajności są obciążeniem:
| Element | Koszt |
| Licencjonowany film z drona w nagłówku strony głównej (640 × 360, MP4 1 178 474 B) | 79% bajtów strony (po r740 86%) |
| Zdjęcie tła nagłówka z krzywą tonalną (filtr SVG) i efektem fali w WebGL | obraz 1. ekranu + chunk 4,15 kB gz |
| Trzy kroje: Anton (H1), Inter Tight 600 (H2/H3), Inter zmienny 400–600 (tekst) | 6 plików WOFF2, na stronie 5 |
| Animacje: wejście tekstów nagłówka, składanie karty przy przewijaniu (od 1200 px) | JS w pliku wejściowym |
| Analityka bez cookies (wtyczka Insights na naszym serwerze), opcjonalnie GA4 z banerem | 1 skrypt + 1 żądanie track |
Ścisła CSP bez unsafe-inline | brak asynchronicznego CSS przez onload |
Diagnoza
1. Raport właściciela (PSI, 03.11.2026 10:27:30). Diagnostyki komputer: „Wymuszone przeformatowanie”, „Drzewo zależności
sieciowych”, „Efektywny czas przechowywania w pamięci podręcznej” (szac. 77 KiB), „Dostarczanie obrazów” (41 KiB), „Żądania
blokujące renderowanie”. Telefon: pamięć podręczna 132 KiB, obrazy 11 KiB, blokowanie renderowania 80 ms, drzewo zależności.
2. Odtworzenie tym samym silnikiem. API PSI bez klucza zwróciło:
HTTP 429: Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service
'pagespeedonline.googleapis.com' for consumer 'project_number:583797351490'.
Mierzyliśmy więc lokalnie Lighthouse 13.6.0 na produkcji (HTTPS), 3 × telefon i 3 × komputer, na przemian, z domyślnym
profilem telefonu i profilem komputera, z czterema kategoriami raportu i zapisem pełnego wyniku do analizy.
| 03.11.2026, przed poprawką | Telefon (3 przebiegi) | Komputer (3 przebiegi) |
| Wydajność / Dostępność / Dobre praktyki / SEO | 99 / 100 / 100 / 100 (×3) | 100 / 100 / 100 / 100 (×3) |
| FCP | 0,97–1,05 s | 0,27–0,29 s |
| LCP | 2,25–2,26 s (wynik metryki 0,94) | 0,40 s |
| TBT | 4–29 ms | 0 ms |
| CLS | 0 | 0,0001 |
| Speed Index | 1,33–1,53 s | 0,52–0,55 s |
| Odpowiedź serwera (dokument) | 334–429 ms | 337–405 ms |
| Element LCP | <p class="reveal …"> (wiersz nad H1) | <img> zdjęcia tła, AVIF 1600w |
| Waga / żądania | 1 455 KiB / 18 | 1 400 KiB / 20 |
| Obce domeny | 0 | 0 |
Diagnostyki i ich zasoby (z JSON, ten sam zestaw co u właściciela):
| Diagnostyka | Telefon | Komputer | Zasoby |
| Pamięć podręczna | 132 KiB | 77 KiB | tylko pliki AVIF z uploads/: hero 2400w (124 831 B) i karta 400w na telefonie; hero 1600w (48 842 B) i 3 karty po ok. 10 KB na komputerze; cacheLifetimeMs: 0 |
| Dostarczanie obrazów | 11 KiB | 41 KiB | plakat filmu /videos/hero-rzeszow.jpg (JPEG 50 130 B): „nowoczesny format” 11 730 B; na komputerze też „640×360 wyświetlany jako 293×165” 39 633 B |
| Blokowanie renderowania | tak | tak | app-BLqXUjcx.css (13 087 B przesłane), metricSavings FCP 0, LCP 0 |
| Wymuszone przeformatowanie | brak | 30,7 ms | app-BMWsm8eR.js (choreografia przewijania, od 1200 px) |
| Drzewo zależności | 561 ms | 585 ms | HTML → app.js → chunki loader, stats, hero-video; HTML → CSS → Inter Tight latin/pl i Inter pl (bez preload) |
Waga strony głównej na telefonie według typu (przesłane bajty):
| Typ | Żądania | KiB |
Wideo (MP4, priorytet Low, po load) | 1 | 1 152,4 |
| Obrazy | 3 | 180,9 |
| Czcionki | 5 | 81,5 |
| HTML (gzip) | 1 | 18,3 |
| CSS (gzip) | 1 | 12,8 |
| JS (gzip) | 5 | 8,1 |
Inne (favicon, track) | 2 | 1,2 |
3. Kto jest elementem LCP na telefonie. Przeglądarka z obserwatorem PerformanceObserver typu
largest-contentful-paint na produkcji (412 × 823, DPR 1,75, jak profil Lighthouse):
| Telefon | Komputer (1350 × 940) |
| Prostokąt zdjęcia tła | 412 × 872 px od góry: cały viewport | 1350 × 800 px |
| Kandydaci LCP | tekst przycisku na pasku telefonu (568 ms, 2 780 px²), potem <p class="reveal"> (616 ms, 17 296 px²) | <img> hero-bg 1600w AVIF (496 ms, 1 080 000 px²) |
| Plik zdjęcia tła | 2400w AVIF (122 KiB) | 1600w AVIF (48 KiB) |
Chrome pomija przy LCP elementy niewidoczne (opacity: 0) i takie, które zakrywają cały viewport (traktuje je jak tło).
Na telefonie zdjęcie tła odpada, a wiersz nad H1 zaczyna od przezroczystości.
4. Czy 100 trzyma się poza stroną główną. Pięć podstron różnych typów, po jednym przebiegu na profil, 03.11.2026
10:54–10:57 (przed poprawką):
| Podstrona | Telefon | LCP telefon | Element LCP na telefonie | Komputer | Waga (telefon) |
| Usługa: strony internetowe dla firm | 100 / 100 / 100 / 100 | 0,99 s | link w pasku telefonu | 100 / 100 / 100 / 100 | 159 KiB |
| Kontakt (z formularzem) | 100 / 100 / 100 / 100 | 1,00 s | link w pasku telefonu | 100 / 100 / 100 / 100 | 158 KiB |
| Poradnik (przyspieszenie sklepu) | 100 / 100 / 100 / 100 | 1,50 s | okruszek (aria-current) | 100 / 100 / 100 / 100 | 166 KiB |
| Realizacja (karta projektu) | 100 / 100 / 100 / 100 | 0,91 s | link w pasku telefonu | 100 / 100 / 100 / 100 | 193 KiB |
| O nas (film w tle nagłówka) | 99 / 100 / 100 / 100 | 2,18 s | <video>: plakat JPEG 1280 × 720, 149,9 KiB | 100 / 100 / 100 / 100 | 3 948 KiB |
Na podstronach bez filmu waga to 158–193 KiB, a elementem LCP telefonu był mały element, bo teksty nagłówka też wchodziły
z przezroczystości. Strona „O nas” ma na telefonie ten sam problem co strona główna, w innej postaci: elementem LCP jest
plakat filmu w tle (JPEG 150 KiB bez wskazówki priorytetu; Lighthouse szacuje 104 KiB oszczędności i 450 ms LCP).
5. Ile brakowało. Wagi wyniku wydajności w Lighthouse 13.6.0: FCP 10, LCP 25, TBT 30, CLS 25, SI 10. LCP na telefonie
liczony jest z punktami kontrolnymi p10 = 2 500 ms i mediana = 4 000 ms. Przy 2 250 ms wynik metryki to 0,94, więc
0,75 + 0,25 × 0,94 = 0,985 i po zaokrągleniu 99. Na 100 (≥ 0,995) LCP musi mieć wynik ≥ 0,98, czyli zejść poniżej ok. 1,9 s
(1 800 ms daje 0,985, 1 500 ms 0,996). Po r740: LCP 1 650 ms, wynik metryki 0,99, kategoria 100.
Przyczyna
Lighthouse nie mierzy LCP na wolnym łączu, tylko go symuluje (Lantern): bierze z prawdziwego, szybkiego wczytania
obserwowany moment LCP i zasoby, których pobieranie zaczęło się przed nim, a potem przelicza ich czas dla 150 ms RTT,
1,6 Mb/s i CPU ×4. W naszych przebiegach na telefonie:
| Przebieg 1 | Przebieg 2 | Przebieg 3 |
Obserwowane FCP / LCP / load | 524 / 608 / 531 ms | 618 / 665 / 653 ms | 486 / 517 / 515 ms |
| Symulowane FCP / LCP | 1 050 / 2 257 ms | 998 / 2 253 ms | 975 / 2 253 ms |
Obserwowany LCP (wiersz nad H1, który wchodził z opacity: 0) wypadał 31–84 ms po FCP, czyli już po zdarzeniu load.
Przed nim przeglądarka zdążyła zażądać prawie wszystkiego: zdjęcia tła w wersji 2400w (122 KiB, fetchpriority="high"),
plakatu filmu (49 KiB, JPEG), pięciu czcionek (81,5 KiB), stylów i skryptów. Symulacja dolicza te bajty do czasu tekstu
i na 1,6 Mb/s wychodzi 2,25 s. Zdjęcie tła nie jest przy tym elementem LCP (zakrywa cały ekran), więc jego 122 KiB
spowalniało wynik, nic nie dając. Na komputerze LCP to samo zdjęcie (nie zakrywa całego ekranu), 1600w, 48 KiB, pobrane
w 33–50 ms przy 10 Mb/s.
Co nie było oczywiste
- Na telefonie LCP to wiersz tekstu, nie zdjęcie, a liczą się bajty pobrane przed nim. Zdjęcie tła ma na telefonie 412 × 872 px
przy ekranie 412 × 823 px, więc Chrome uznaje je za tło i nie zgłasza jako kandydata LCP. Na komputerze (1350 × 800 px
na 940 px wysokości) to samo zdjęcie jest LCP. Kandydatem na telefonie zostaje
<p class="reveal">, a @keyframes reveal
zaczynał od opacity: 0. fetchpriority="high" na zdjęciu tła na telefonie nie pomagał LCP, tylko zabierał pasmo,
a symulacja Lighthouse doliczała jego 122 KiB do czasu tekstu. Poprawka (r740) usuwa opacity: 0 z wejścia; od razu
widoczny tekst przestaje być największym elementem i LCP telefonu przejmuje plakat filmu (<video> 640 × 360 w nagłówku),
więc to on dostał preload. Po wdrożeniu (13:09–13:11) Lighthouse pokazuje na telefonie jako element LCP <video>
z plakatem AVIF, „Wykrywanie żądań LCP” spełnione we wszystkich trzech punktach (fetchpriority, odkrywalny w HTML,
bez lazy), LCP 1,65 s. Od r743 kadr jest osobnym <img srcset>, a po r747 to on jest elementem LCP telefonu
(LCP 1,50–1,51 s). Na podstronach elementem LCP telefonu jest teraz akapit wstępu albo zdjęcie projektu zamiast
linku w pasku telefonu: tekst nagłówka liczy się od pierwszej klatki.
- AVIF bez nagłówka cache, WebP z rocznym. Obraz bazowy ma w
server-opts.d/performance.conf regułę cache dla
css|js|jpe?g|png|gif|…|webp|…|mp4|…|svgz?, bez avif. AVIF trafia do domyślnej lokalizacji z nagłówkami
bezpieczeństwa i bez Cache-Control (w nginx add_header nie dziedziczy się do location z własnym add_header).
Przeglądarka dostaje AVIF, bo jest pierwszym <source>, więc właśnie lepszy format nie zostaje w pamięci podręcznej.
Dowód: curl -sI z 03.11.2026 10:38 (AVIF: bez Cache-Control, z x-frame-options; WebP tego samego zdjęcia:
public, max-age=31536000, immutable). Poprawka w r740: własna reguła location ~* \.avif$ w obrazie (niżej). Po
przebudowie obrazu (curl -sI 03.11.2026 13:08) AVIF z uploads/ i plakat AVIF mają public, max-age=31536000,
immutable, a pozycja „pamięć podręczna” zniknęła z raportu na obu profilach.
- CSS zostaje blokujący, bo asynchroniczny CSS łamie CSP. W trybie asynchronicznym wtyczka Vite dla Crafta wypisuje
<link rel="stylesheet" media="print" onload="this.media='all'">. CSP script-src 'self' bez unsafe-inline blokuje
atrybuty on*, więc arkusz zostałby przy media="print", a strona bez stylów. Szablon używa
craft.vite.script('src/js/app.js', false): zwykły <link rel="stylesheet"> (12,52 kB gz). PSI pokazuje „Żądania
blokujące renderowanie” (u właściciela szac. 80–130 ms na telefonie), ale metricSavings dla FCP i LCP w JSON to 0.
Po r747 to jedyna podpowiedź w raporcie (PSI 14:21: czerwona na telefonie, pomarańczowa na komputerze). preload as=style
jej nie usuwa: zostaje przy każdym zewnętrznym arkuszu, a znika dopiero przy CSS wpisanym w HTML. Przy style-src 'self'
wymagałoby to skrótu (hash) każdego bloku <style> w CSP; decyzja jest otwarta.
- Craft dopisuje do HTML skrypt inline, którego CSP nie przepuszcza. Gdy podczas żądania powstaje zadanie kolejki
(pierwsza transformacja obrazu), Craft dokleja inline skrypt uruchamiający kolejkę. CSP go blokuje: błąd w konsoli
(w Dobrych praktykach audyt
errors-in-console), zadania czekają, a każda odsłona dokłada
zadania kolejnych wersji srcset (w innym naszym projekcie kolejka urosła do 2 441 zadań). Rozwiązanie:
stały worker queue/listen jako usługa s6 w obrazie i runQueueAutomatically(false). Obraz pierwszego ekranu ma
wersje generowane od razu (Pictures::build(…, $immediately)), więc jego adres to plik, a nie adres generatora
z przekierowaniem.
- Film to 79% wagi strony (po r740 86%) i nie wpływa na wynik. 1 152 KiB z 1 455 KiB na telefonie (po r740 z 1 340 KiB).
preload="none", start dopiero po
load (hero-video.js), priorytet Low: Lighthouse go pobiera, ale żadna metryka na tym nie traci. Nasz budżet
„strona główna < 0,5 MB” bez filmu jest spełniony (303 KiB na telefonie, po r740 188 KiB), z filmem nie. Plakat JPEG
640 × 360 wyświetlany w ramce 293 × 165 px to cała pozycja „Dostarczanie obrazów”; po r740 plakat AVIF (26 049 B) w tej
samej ramce na komputerze nadal daje 20 KiB, bo atrybut poster nie przyjmuje srcset. r743 zastępuje go zwykłym
<img srcset> (AVIF 320w, 400w, 640w) nad filmem; po r747 pozycja zniknęła z raportu.
sizes liczony uczciwie dał telefonowi największy plik. Pas nagłówka na telefonie jest wyższy niż szeroki, zdjęcie 16:9
z object-cover jest rysowane na ok. 2,8 szerokości ekranu, więc preset miał sizes="(max-width: 767px) 280vw, …". Przy
412 px i DPR 1,75 to ok. 2 000 px fizycznych: przeglądarka bierze 2400w (122 KiB). Dla przyciemnionego tła, które na
telefonie nie jest nawet elementem LCP, to za dużo. r740 świadomie zaniża sizes do 160vw: wersja 1280w, 31 KiB.
- Lokalny Lighthouse przez HTTP myli, a API PSI bez klucza ma limit. Na lokalnym stosie przez HTTP i z
noindex
Dobre praktyki spadają do ok. 78 (brak HTTPS), SEO do ok. 66 (is-crawlable); na stagingu przez HTTPS ta sama strona
miała 98/100/100/69. Wynik ma sens tylko na HTTPS i bez noindex. API PSI bez klucza 03.11.2026 zwracało 429 (limit
dzienny, rano i o 13:16); Lighthouse 13.6.0 uruchomiony lokalnie na produkcji dał te same diagnostyki co oba raporty PSI.
Wynik waha się też między przebiegami: podstrona kontaktu na telefonie dała raz 99 (TBT 124 ms), w dwóch kolejnych
przebiegach 100 (TBT 45–47 ms); po r747 3 z 3 przebiegów 100. Dlatego podajemy rozrzut, a nie jeden najlepszy pomiar.
- Poprawka dla telefonu przesunęła diagnostykę na komputer. r740 zamienia na zdjęciu tła
priority (loading="eager"
i fetchpriority="high") na nową opcję eager (bez fetchpriority) na wszystkich szerokościach, bo na telefonie zdjęcie
nie jest elementem LCP. Na komputerze jest: w 3 z 3 przebiegów po wdrożeniu „Wykrywanie żądań LCP” ma
priorityHinted: false (wynik audytu 0), tak samo w raporcie PSI z 13:05. Metryka na tym nie traci (LCP 0,36–0,40 s,
wynik 1), ale raport ma czerwoną pozycję, której rano nie było. fetchpriority na <img> nie zależy od zapytania
o media, więc priorytet tylko na komputerze wymaga osobnej drogi. r744 dodaje <link rel="preload" as="image"
type="image/avif" imagesrcset=… imagesizes=… fetchpriority="high" media="(min-width: 768px)"> dla zdjęcia tła; po r747
w 3 z 3 przebiegów na komputerze „Wykrywanie żądań LCP” ma wszystkie trzy punkty spełnione.
- „Wymuszone przeformatowanie” bez winnego skryptu było fałszywym alarmem. Po r743 (kod najpierw czyta wymiary, potem
pisze; pierwszy pomiar w raporcie
ResizeObserver) raporty właściciela z 14:03/14:04 nadal pokazywały wymuszone
przeformatowanie, tym razem „[nieprzypisane]”: 63 ms na telefonie, 139 ms na komputerze. Ślad Lighthouse pokazał, że to
pierwsze zwykłe rozmieszczenie strony (849 obiektów, ok. 30 ms), które silnik śladu uznał za wymuszone, bo w tym samym
czasie na wątku w tle parsował się chunk hero-video (v8.parseOnBackground); silnik nie rozróżnia wątków. r747
ładuje chunki filmu i statystyk dopiero po load (film i tak startuje dopiero wtedy). Po r747: audyt zaliczony w 6 z 6
przebiegów strony głównej.
- „Drzewo zależności sieciowych” zgłasza każdy łańcuch krytycznych żądań, a
preload go przerywa. Audyt ma stan
fail przy każdym łańcuchu o długości co najmniej 2 z żądań krytycznych (HTML → CSS → czcionki, HTML → app.js →
chunki). Żądania z preload/modulepreload traktuje jako niekrytyczne. Skaner wstępny przeglądarki i tak znajduje oba
pliki od razu, więc wskazówki nie zmieniają kolejności pobierania, tylko sposób liczenia. r747 dodaje w szablonie
modulepreload dla pliku wejściowego i preload as=style dla arkusza; po r747 audyt zaliczony w 6 z 6 przebiegów.
Rozwiązanie
Obrazy: jeden komponent, nazwane presety (config/sitekit.php → images.presets, _components/picture.twig,
sitekit\Craft\Pictures):
'heroBg' => [
'ratio' => [16, 9],
'widths' => [960, 1280, 1600, 1920, 2400],
'sizes' => '(max-width: 767px) 160vw, (max-width: 1279px) 160vw, 100vw', // do r740: 280vw na telefonie
'quality' => ['avif' => 55, 'webp' => 70],
],
'card' => ['ratio' => [4, 3], 'widths' => [320, 480, 720, 960],
'sizes' => '(min-width: 1280px) 400px, (min-width: 768px) 45vw, 100vw',
'quality' => ['avif' => 60, 'webp' => 72]],
<picture>
<source type="image/avif" srcset="… 960w, … 1280w, … 1600w, … 1920w, … 2400w" sizes="…">
<img src="…_1600x900_…/hero-bg.webp" srcset="… (WebP)" sizes="…" width="2400" height="1350"
alt="" decoding="async" loading="eager" fetchpriority="high"> {# do r740; od r740 tło bez fetchpriority #}
</picture>
Poniżej pierwszego ekranu loading="lazy"; zawsze width/height (CLS 0); AVIF tylko, gdy sterownik obrazów go zapisze.
Czcionki (src/css/app.css): własny serwer, WOFF2, font-display: swap, osobny plik z polskimi znakami przez
unicode-range; preload tylko dwóch plików pierwszego ekranu (Anton do H1, Inter do tekstu):
@font-face {
font-family: Inter; font-weight: 400 600; font-display: swap;
src: url('../fonts/inter-pl-var.woff2') format('woff2');
unicode-range: U+0104-0107, U+0118-0119, U+0141-0144, U+015A-015B, U+0179-017C;
}
| Plik | Rozmiar | Preload |
| anton-latin-400 | 18,18 kB | tak |
| inter-latin-var (400–600) | 34,54 kB | tak |
| inter-tight-latin-600 | 22,13 kB | nie |
| anton-pl / inter-pl / inter-tight-pl | 4,06 / 3,51 / 1,88 kB | nie |
CSS i JS (Tailwind 4 z @import 'tailwindcss' source(none) i jawnymi @source, bez domyślnej palety): jeden plik CSS
12,52 kB gz. JS wejściowy 2,19 kB gz (type="module", więc odroczony); reszta to chunki ładowane tylko, gdy w HTML jest
ich hak:
const modules = {
'form[data-inquiry]': () => import('./components/inquiry.js'),
'[data-hero-video]': () => import('./components/hero-video.js'),
'[data-consent]': () => import('./components/consent.js'),
'body[data-stats-src]': () => import('./components/stats.js'),
};
// efekt fali: dopiero po 'load', potem requestIdleCallback; tylko (hover: hover) and (pointer: fine),
// nigdy przy prefers-reduced-motion ani Save-Data
Budżet sprawdzany przy każdej kompilacji (kompilacja z przekroczonym budżetem kończy się błędem): CSS ≤ 25 kB gz,
JS wejściowy ≤ 15 kB gz, chunk ≤ 60 kB gz, plik czcionki ≤ 80 kB. Stan z 03.11.2026: 12,52 / 2,19 / 0,17–4,15 / 1,88–34,54 kB.
Film: <video muted loop playsinline preload="none" poster="…" width="640" height="360">, aspect-video, start po
load, pauza poza ekranem i w ukrytej karcie, przycisk pauzy (WCAG 2.2.2), nigdy przy prefers-reduced-motion ani
navigator.connection.saveData. Ten sam wpis konfiguracji (homeVideo) zasila VideoObject w danych strukturalnych.
Nagłówki (produkcja, 03.11.2026):
HTML: HTTP/2 200, content-encoding: gzip, bez Cache-Control
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
font-src 'self'; connect-src 'self'; media-src 'self'; manifest-src 'self'; worker-src 'self';
object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests
strict-transport-security: max-age=31536000; includeSubDomains · cross-origin-opener-policy: same-origin
CSS, JS, WOFF2, cache-control: public, max-age=31536000, immutable (nazwy z hashem treści z Vite)
WebP, JPG, MP4:
AVIF (uploads/): brak Cache-Control do r740 (10:38); od r740 public, max-age=31536000, immutable (13:08)
Dostępność i SEO (100/100): automatyczny test dostępności (axe, reguły WCAG 2.0–2.2 poziomu A i AA) przechodzi każdy
adres z /sitemap.xml, stronę potwierdzenia formularza i stronę 410; czeka na koniec animacji wejścia, inaczej mierzyłby
kontrast półprzezroczystego tekstu. Własny audyt SEO (polecenie konsoli Crafta) pobiera każdą stronę z mapy jak robot:
status 200, jeden h1, język, canonical, robots, dane strukturalne, osierocone strony. Oba uruchamiamy po każdej
zmianie szablonów i treści; ostatni wpis w dzienniku projektu z 03.11.2026: 156 testów dostępności bez naruszeń
(12 pominiętych, bo dana funkcja nie występuje na stronie), audyt SEO bez błędów.
Strona główna ma Organization+ProfessionalService, WebSite, WebPage, VideoObject, FAQPage; meta robots
index, follow, max-image-preview:large, max-snippet:-1; robots.txt z grupami robotów wyszukiwarek i asystentów AI,
/llms.txt.
Poprawka ostatniego punktu na telefonie (rewizja r740 z 03.11.2026, na produkcji przed raportem z 13:05). Cztery zmiany:
/* app.css: wejście tekstów nagłówka bez startowej przezroczystości */
@keyframes reveal {
from {
- opacity: 0;
transform: translateY(12px);
}
}
// config/sitekit.php: tło nagłówka na telefonie w wersji 1280 (31 KB zamiast 124 KB)
-'sizes' => '(max-width: 767px) 280vw, (max-width: 1279px) 160vw, 100vw',
+'sizes' => '(max-width: 767px) 160vw, (max-width: 1279px) 160vw, 100vw',
{# home-hero.twig: tło bez wysokiego priorytetu (nowa opcja eager: od razu, ale zwykły priorytet) #}
- priority: true,
+ eager: true,
{# plakat filmu jako AVIF (26 049 B zamiast 50 130 B), wczytywany z wyprzedzeniem tylko na telefonach #}
{% do view.registerLinkTag({ rel: 'preload', as: 'image', href: video.pagePoster,
fetchpriority: 'high', media: '(max-width: 767px)' }) %}
# Docker/craft/nginx/avif-cache.conf, kopiowany do server-opts.d obrazu
location ~* \.avif$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri /index.php?$query_string;
}
immutable jest tu bezpieczne, bo nazwa pliku zmienia się razem z treścią: transformacje Crafta mają parametry w ścieżce,
a importer nie podmienia pliku pod tą samą nazwą.
Usunięcie ostatnich podpowiedzi (rewizje r743, r744 i r747 z 03.11.2026, na produkcji przed raportem z 14:21):
{# r743, home-hero.twig: kadr filmu jako responsywny <img> nad filmem zamiast atrybutu poster (ten nie przyjmuje srcset);
hero-video.js ukrywa go przy zdarzeniu playing #}
<img class="absolute inset-0 size-full object-cover" src="/videos/hero-rzeszow.avif"
srcset="/videos/hero-rzeszow-320.avif 320w, /videos/hero-rzeszow-400.avif 400w, /videos/hero-rzeszow.avif 640w"
sizes="(min-width: 768px) 300px, 50vw" width="640" height="360" alt="" fetchpriority="high" decoding="async"
data-hero-video-still>
{# r744: zdjęcie tła z priorytetem tylko na komputerze, gdzie jest elementem LCP #}
<link rel="preload" as="image" type="image/avif" media="(min-width: 768px)" fetchpriority="high"
imagesrcset="…/hero-bg.avif 960w, … 2400w" imagesizes="…">
// r747, app.js: chunki bez pracy przed `load` ładowane po nim; ich parsowanie w tle nie nakłada się już
// na pierwsze rozmieszczenie strony (fałszywe „wymuszone przeformatowanie”, pułapka 9)
const lateModules = {
'[data-hero-video]': () => import('./components/hero-video.js'),
'body[data-stats-src]': () => import('./components/stats.js'),
};
start(modules);
afterLoad(() => start(lateModules));
{# r747, base.twig: wskazówki dla pliku wejściowego i arkusza z manifestu Vite (pułapka 10) #}
{% for link in sk_vite_preloads('src/js/app.js') %}{{ tag('link', link) }}{% endfor %}
{# → <link rel="modulepreload" href="…/app-….js" crossorigin> (ten sam tryb CORS co skrypt, inaczej plik pobiera się dwa razy)
<link rel="preload" as="style" href="…/app-….css"> #}
Do tego r743 usuwa prawdziwe wymuszone przeformatowania: baner zgody najpierw odczytuje wymiary, potem zapisuje style,
a składanie karty przy przewijaniu i efekt fali robią pierwszy pomiar w raporcie ResizeObserver, czyli po obliczeniu
układu. Strona „O nas” (r744) dostaje kadr filmu w tle jako <img srcset> w AVIF w skali szarości (640w ok. 10 KiB,
1280w ok. 39 KiB zamiast JPEG 150 KiB) z preload na telefonach.
Rezultaty
Produkcja, 03.11.2026; „przed” = PSI 10:27 i Lighthouse 10:36–10:57, „po r740” = PSI 13:05 i Lighthouse 13:09–13:15,
„po r747” = PSI 14:21 i Lighthouse 14:23–14:28.
| Etykieta | Przed r740 | Po r740 | Po r747 |
| PSI właściciela, komputer | 100 / 100 / 100 / 100, „Przeglądanie agentowe” 3/3 | 100 / 100 / 100 / 100, 3/3 | 100 / 100 / 100 / 100, 3/3 |
| PSI właściciela, telefon | 99 / 100 / 100 / 100, 3/3 | 100 / 100 / 100 / 100, 3/3 | 100 / 100 / 100 / 100, 3/3 |
| Lighthouse 13.6.0, strona główna, telefon | 99 / 100 / 100 / 100 w 3 z 3 pomiarów | 100 / 100 / 100 / 100 w 3 z 3 pomiarów | 100 / 100 / 100 / 100 w 3 z 3 pomiarów |
| Lighthouse 13.6.0, strona główna, komputer | 100 / 100 / 100 / 100 w 3 z 3 pomiarów | 100 / 100 / 100 / 100 w 3 z 3 pomiarów | 100 / 100 / 100 / 100 w 3 z 3 pomiarów |
| LCP telefon / komputer | 2,25–2,26 s (wynik 0,94) / 0,40 s | 1,65–1,66 s (wynik 0,99) / 0,36–0,40 s | 1,50–1,51 s (wynik 1) / 0,36–0,37 s |
| Element LCP telefonu | <p class="reveal"> nad H1 | <video>, plakat AVIF 26 049 B z preload | <img> kadru filmu, AVIF z srcset i preload |
| FCP telefon / komputer | 0,97–1,05 s / 0,27–0,29 s | 0,98–1,02 s / 0,27–0,29 s | 1,01–1,04 s / 0,27–0,28 s |
| Speed Index telefon / komputer | 1,33–1,53 s / 0,52–0,55 s | 1,20–1,30 s / 0,49–0,55 s | 1,25–1,35 s / 0,48–0,51 s |
| TBT / CLS | 0–29 ms / 0–0,0001 | 0–24 ms / 0–0,0001 | 0–87 ms / 0–0,0001 |
| Obrazy na telefonie | 3 pliki, 180,9 KiB (tło 2400w 122 KiB, plakat JPEG 49 KiB) | 3 pliki, 65,8 KiB (tło 1280w, plakat AVIF) | 3 pliki, 53,6 KiB |
| Waga / żądania, telefon | 1 455 KiB / 18 | 1 340 KiB / 18 | 1 328 KiB / 18 |
| Diagnostyki, telefon | pamięć podręczna 132 KiB, obrazy 11 KiB, blokowanie renderowania, drzewo zależności | blokowanie renderowania (metricSavings 0), drzewo zależności | blokowanie renderowania (metricSavings 0) |
| Diagnostyki, komputer | pamięć podręczna 77 KiB, obrazy 41 KiB, wymuszone przeformatowanie, blokowanie renderowania, drzewo zależności | obrazy 20 KiB, wykrywanie żądań LCP (nowe, pułapka 8), wymuszone przeformatowanie (2 z 3 przebiegów), blokowanie renderowania, drzewo zależności | blokowanie renderowania (metricSavings 0) |
| Podstrony bez filmu (usługa, kontakt, poradnik, realizacja) | 100 / 100 / 100 / 100 na obu profilach, LCP telefon 0,91–1,50 s, 158–193 KiB | 100 / 100 / 100 / 100 na obu profilach (kontakt na telefonie 1 z 3 przebiegów 99), LCP telefon 0,94–1,65 s | 100 / 100 / 100 / 100 na obu profilach (kontakt na telefonie 3 z 3), LCP telefon 0,95–1,65 s |
Podstrona z filmem w tle (/o-nas) | telefon 99 (LCP 2,18 s, plakat JPEG 150 KiB), komputer 100 | telefon 99 w 2 z 2 przebiegów (LCP 2,10 s, ten sam plakat), komputer 100 | telefon 100 w 3 z 3 (LCP 1,46–1,47 s), komputer 100 |
| JS na stronie głównej | 5 plików, 8,1 KiB przesłane (wejście 2,19 kB gz) | bez zmian | bez zmian (chunki filmu i statystyk po load) |
| CSS | 1 plik, 12,8 KiB przesłane | bez zmian | bez zmian |
| Czcionki | 5 plików, 81,5 KiB | bez zmian | bez zmian |
| Obce domeny | 0 | 0 | 0 |
| Agentic Browsing | 3/3: drzewo dostępności, CLS, llms.txt (WebMCP i manifest ARD nie dotyczą) | 3/3 | 3/3 |
LCP podstron na telefonie wzrósł po r740 liczbowo (np. realizacja 0,91 → 1,65 s), bo elementem LCP jest teraz właściwa
treść (akapit wstępu, zdjęcie projektu), a nie mały link w pasku; wynik metryki zostaje 0,99–1.
Co zostało
Stan po r747 (PSI 03.11.2026 14:21, Lighthouse 14:23–14:28):
| Diagnostyka / temat | Stan | Dlaczego zostaje albo co dalej |
| Żądania blokujące renderowanie | 1 plik CSS, 12,52 kB gz; jedyna podpowiedź na stronie głównej | świadomie: asynchroniczny CSS wymaga onload inline, CSP go blokuje; podpowiedź znika tylko przy CSS w HTML, co przy style-src 'self' wymaga skrótów w CSP (pułapka 3; decyzja otwarta); PSI szacuje na telefonie 130 ms, metricSavings w JSON strony głównej to 0 dla FCP i LCP |
/o-nas: wykrywanie żądań LCP | na obu profilach elementem LCP jest sam <video> filmu w tle (po starcie filmu), nie kadr | <video> nie przyjmuje fetchpriority; wynik 100, LCP telefon 1,46–1,47 s; strona pobiera w teście 3,6–3,8 MiB (MP4 HD) |
| Podstrony: dostarczanie obrazów (telefon) | 13–43 KiB na stronę (zdjęcia kart i projektów) | wynik 100; do przeglądu presetów card przy okazji |
| Filmy | strona główna 1 152 KiB po load, także na telefonie | autoodtwarzanie na telefonach zostaje (decyzja właściciela z 03.11.2026) |
| Kompresja | gzip poziom 6 z obrazu bazowego, bez Brotli | przeliczone lokalnie: HTML 18 100 → 15 653 B (Brotli 5) / 13 784 B (Brotli 11), CSS 12 851 → 11 121 B (Brotli 11); nasze zasady przewidują Brotli, PSI (usesCompression) tego nie wymaga |
Rezerwowa czcionka z size-adjust | brak | CLS 0 bez niej (nasze zasady przewidują dopasowany fallback) |
| Dane CrUX | „Brak danych” | sprawdzić w PSI i Search Console po kilku tygodniach ruchu |
| Naprawione 03.11.2026 | AVIF bez Cache-Control (r740); wykrywanie żądań LCP na komputerze (r744); dostarczanie obrazów na komputerze, plakat 640 w ramce 293 (r743); wymuszone przeformatowanie (r743 i r747); drzewo zależności sieciowych (r747); /o-nas 99 na telefonie (r744) | pozycje zniknęły z raportu i z naszych pomiarów |
Wnioski
- Wynik 100 wynika z naszych ustawień domyślnych: komponent obrazów z presetami, budżet przy kompilacji, ścisła CSP, zero
zewnętrznych zasobów. Na stronie z filmem, trzema krojami i WebGL brakowało na telefonie jednego punktu.
- Element LCP zależy od urządzenia: na komputerze zdjęcie, na telefonie tekst (po poprawce kadr filmu), bo zdjęcie na cały
ekran jest tłem. Optymalizacja „pod LCP” wymaga sprawdzenia, kto nim naprawdę jest, na każdym profilu, i to po każdej
zmianie: odebranie priorytetu zdjęciu na telefonie dało nową diagnostykę na komputerze.
- Animacje wejścia z
opacity: 0 na elementach pierwszego ekranu przesuwają LCP; ruch bez przezroczystości daje ten sam efekt
wizualny bez kosztu.
- Konfiguracja serwera z obrazu bazowego nie zna każdego formatu: nagłówki cache trzeba sprawdzić
curl -I dla każdego typu
pliku, który strona wysyła.
- Część diagnostyk Lighthouse to skutek sposobu liczenia: „wymuszone przeformatowanie” bez źródła może być parsowaniem
w tle, a „drzewo zależności” liczy łańcuchy żądań bez
preload. Zanim zmieni się kod, warto zajrzeć do śladu.