Hugo Image Processing: Jak odchudziłem stronę o 98%

Spis treści
Wstęp#
Obrazy to zazwyczaj największa przeszkoda w osiągnięciu wysokiej wydajności stron internetowych. Choć Hugo generuje z natury szybkie witryny statyczne, nieoptymalizowane grafiki potrafią całkowicie zniweczyć tę przewagę. Postanowiłem przyjrzeć się temu bliżej na przykładzie własnej strony.
Analiza w Chrome DevTools jasno wskazała źródło problemu. Obrazy stanowiły aż dziewięćdziesiąt sześć procent całego transferu, co dawało prawie piętnaście megabajtów danych. Namierzyłem trzy duże grafiki w formacie PNG, które generowały większość tego ruchu i wydłużały czas ładowania do ponad czterech sekund. Dla użytkowników mobilnych oznaczało to długie oczekiwanie i niepotrzebne zużycie pakietu danych.
W tym artykule przejdziemy przez kompleksowy proces optymalizacji w środowisku Hugo. Pokażę wam sposób na znaczną redukcję rozmiaru plików i przyspieszenie ładowania strony przy zachowaniu wysokiej jakości wizualnej. Zamiast ręcznej obróbki w edytorach graficznych wykorzystamy do tego wbudowane w Hugo mechanizmy przetwarzania obrazów.
Diagnoza problemu: Analiza przed optymalizacją#
Zacznijmy od twardych danych. Rzut oka na plik HAR w Chrome DevTools nie pozostawił złudzeń. Przesyłaliśmy prawie piętnaście megabajtów danych graficznych, a sama transmisja tych plików zajmowała ponad cztery sekundy. Po głębszej analizie zidentyfikowałem cztery główne przyczyny tego stanu rzeczy.
Po pierwsze, format plików był źle dobrany do ich zawartości. Używałem formatu PNG do zrzutów ekranu. Choć PNG świetnie radzi sobie z prostą grafiką o ostrych krawędziach, jest bardzo nieefektywny w przypadku zdjęć czy gradientów. Brak kompresji stratnej powoduje, że pliki niepotrzebnie puchną.
Druga kwestia to serwowanie obrazów w pełnej rozdzielczości. Pliki ważące po sześć lub siedem megabajtów sugerują rozdzielczość 4K, czyli prawdopodobnie ponad dwa tysiące osiemset pikseli szerokości. Tymczasem kontener na mojej stronie wyświetla grafikę o maksymalnej szerokości tysiąca dwustu pikseli. W efekcie wysyłaliśmy do przeglądarki piksele, których użytkownik i tak nie był w stanie zobaczyć.
Trzecim grzechem był brak leniwego ładowania, czyli tak zwanego lazy loading. Wszystkie grafiki były pobierane natychmiast przy pierwszym renderowaniu strony. Nawet te, które znajdowały się głęboko w treści artykułu, poza widocznym obszarem ekranu, obciążały łącze w krytycznym momencie startu.
Na koniec zostawiłem problem braku responsywności. Nie stosowałem atrybutu srcset, co oznaczało, że urządzenia mobilne były traktowane tak samo jak desktopy. Telefon z małym ekranem musiał pobierać te same siedmiomegabajtowe giganty, co komputer stacjonarny. Sumarycznie te cztery błędy kosztowały nas prawie piętnaście megabajtów transferu zmarnowanego na zaledwie trzy obrazy.
Rozwiązanie: Hugo Image Processing#
Hugo dysponuje wbudowanym mechanizmem przetwarzania obrazów przez moduł Hugo Pipes. Oznacza to, że nie musimy ręcznie konwertować każdej grafiki w programie graficznym. Cały proces można zautomatyzować bezpośrednio w szablonach Hugo. Przyjrzyjmy się, jak to działa w praktyce.
Przeniesienie obrazów do katalogu assets#
Hugo Image Processing działa wyłącznie na plikach umieszczonych w katalogu assets, a nie w katalogu static. To kluczowa różnica w architekturze Hugo i warto zrozumieć, dlaczego tak jest.
Pliki w katalogu static są kopiowane bez zmian do folderu public podczas budowania strony. Hugo nie przetwarza ich w żaden sposób. Z kolei pliki z katalogu assets są dostępne dla Hugo Pipes, co oznacza, że możemy je zmniejszać, konwertować i optymalizować automatycznie podczas procesu budowania. Dlatego pierwszym krokiem jest przeniesienie wszystkich obrazów z folderu static do odpowiedniej struktury w assets.
Tworzenie shortcode dla responsywnych obrazów#
Teraz tworzymy plik shortcode pod ścieżką layouts/shortcodes/image.html. Dzięki niemu każdy obraz umieszczony w pliku markdown będzie podczas budowania strony automatycznie optymalizowany, skalowany i przygotowany w kilku wersjach. To właśnie ten mechanizm pozwoli nam wygenerować atrybut srcset i wdrożyć leniwe ładowanie bez ręcznej edycji każdego przypadku.
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" | default "Image" }}
{{/* Validate src parameter */}}
{{ if not $src }}
<p class="error">Error: image src parameter is required</p>
{{ else }}
{{ $img := resources.Get $src }}
{{ if $img }}
{{/* Define sizes based on original image width */}}
{{ $originalWidth := $img.Width }}
{{ $sizes := slice }}
{{/* Only generate sizes that are smaller than or equal to original */}}
{{ if ge $originalWidth 800 }}{{ $sizes = $sizes | append 800 }}{{ end }}
{{ if ge $originalWidth 1200 }}{{ $sizes = $sizes | append 1200 }}{{ end }}
{{ if ge $originalWidth 1600 }}{{ $sizes = $sizes | append 1600 }}{{ end }}
{{/* If image is smaller than 800px, use original width */}}
{{ if lt $originalWidth 800 }}{{ $sizes = slice $originalWidth }}{{ end }}
<picture>
{{/* WebP source - good compression, wide support */}}
<source type="image/webp" srcset="
{{- range $i, $size := $sizes -}}
{{- $resized := $img.Resize (printf "%dx webp q80" $size) -}}
{{- $resized.RelPermalink }} {{ $size }}w
{{- if ne $i (sub (len $sizes) 1) }}, {{ end -}}
{{- end -}}"
sizes="(max-width: 800px) 100vw, 1200px">
{{/* Fallback image */}}
{{ $fallbackSize := 1200 }}
{{ if lt $originalWidth 1200 }}{{ $fallbackSize = $originalWidth }}{{ end }}
{{ $fallback := $img.Resize (printf "%dx q80" $fallbackSize) }}
<img
src="{{ $fallback.RelPermalink }}"
alt="{{ $alt }}"
loading="lazy"
decoding="async"
width="{{ $fallback.Width }}"
height="{{ $fallback.Height }}">
</picture>
{{ else }}
<p class="error">Image not found: {{ $src }}</p>
{{ end }}
{{ end }}
Optymalizacja okładek: Własny partial#
Większość szablonów Hugo wyświetla grafiki okładkowe, tak zwane cover images, za pomocą jednego fragmentu kodu. Jest to zazwyczaj {{ partial “cover.html” . }}. Aby zoptymalizować te kluczowe elementy strony, nie musimy edytować każdego widoku z osobna. Wystarczy, że stworzymy naszą własną, ulepszoną wersję tego pliku.
Tworzymy plik layouts/partials/cover.html, który nadpisze domyślne zachowanie motywu. Dzięki temu mechanizmowi, każda okładka wpisu — czy to na stronie głównej, czy w samym artykule — zostanie automatycznie przepuszczona przez nasz proces optymalizacji. To idealny przykład zasady “zdefiniuj raz, używaj wszędzie”, która oszczędza mnóstwo czasu przy utrzymaniu projektu.
{{- $cover := false -}}
{{- $autoCover := default $.Site.Params.autoCover false }}
{{- if index .Params "cover" -}}
{{/* Remove leading slash if exists */}}
{{- $coverPath := strings.TrimPrefix "/" .Params.Cover -}}
{{- if .Resources.GetMatch $coverPath }}
{{- $cover = .Resources.GetMatch $coverPath -}}
{{- else -}}
{{- $resource := resources.Get $coverPath -}}
{{- if $resource -}}
{{- $cover = $resource -}}
{{- else -}}
{{/* Fallback: use absolute URL if resource not found */}}
{{- $cover = absURL .Params.Cover -}}
{{- end -}}
{{- end -}}
{{- else if $.Site.Params.AutoCover -}}
{{- if (not .Params.Cover) -}}
{{- if .Resources.GetMatch "cover.*" -}}
{{- $cover = .Resources.GetMatch "cover.*" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $cover -}}
{{- $coverType := printf "%T" $cover -}}
{{- if ne $coverType "string" -}}
{{/* $cover is a resource - use Hugo Image Processing */}}
{{/* Define sizes based on original image width (no upsampling!) */}}
{{- $originalWidth := $cover.Width -}}
{{- $sizes := slice -}}
{{/* Only generate sizes that are smaller than or equal to original */}}
{{- if ge $originalWidth 800 }}{{ $sizes = $sizes | append 800 }}{{ end -}}
{{- if ge $originalWidth 1200 }}{{ $sizes = $sizes | append 1200 }}{{ end -}}
{{- if ge $originalWidth 1600 }}{{ $sizes = $sizes | append 1600 }}{{ end -}}
{{/* If image is smaller than 800px, use original width */}}
{{- if lt $originalWidth 800 }}{{ $sizes = slice $originalWidth }}{{ end -}}
<picture class="post-cover">
{{/* WebP source - good compression, wide support */}}
<source type="image/webp" srcset="
{{- range $i, $size := $sizes -}}
{{- $resized := $cover.Resize (printf "%dx webp q80" $size) -}}
{{- $resized.RelPermalink }} {{ $size }}w
{{- if ne $i (sub (len $sizes) 1) }}, {{ end -}}
{{- end -}}"
sizes="(max-width: 800px) 100vw, 1200px">
{{/* Fallback image - universal support */}}
{{- $fallbackSize := 1200 -}}
{{- if lt $originalWidth 1200 }}{{ $fallbackSize = $originalWidth }}{{ end -}}
{{- $fallback := $cover.Resize (printf "%dx q80" $fallbackSize) -}}
<img
src="{{ $fallback.RelPermalink }}"
class="post-cover"
alt="{{ .Title | plainify | default " " }}"
title="{{ .Params.CoverCredit | plainify | default "Cover Image" }}"
loading="lazy"
decoding="async"
width="{{ $fallback.Width }}"
height="{{ $fallback.Height }}">
</picture>
{{- else -}}
{{/* $cover is a string URL - fallback to simple img tag */}}
<img src="{{ $cover }}"
class="post-cover"
alt="{{ .Title | plainify | default " " }}"
title="{{ .Params.CoverCredit | plainify | default "Cover Image" }}"
loading="lazy">
{{- end -}}
{{- end -}}
Zabezpieczenie wizualne i wdrożenie w treści#
Sama optymalizacja plików to nie wszystko. Musimy też zadbać o to, by przeglądarka poprawnie wyświetlała nasze nowe, lżejsze grafiki.
Kiedy manipulujemy ładowaniem obrazów, kluczowe jest zachowanie ich naturalnych proporcji, czyli aspect ratio. Jeśli wasz arkusz stylów zawiera ogólne reguły dla elementu img, warto dodać małą poprawkę w pliku static/style.css. Chodzi o wymuszenie automatycznej wysokości przy maksymalnej szerokości.
img {
display: block;
max-width: 100%;
height: auto; /* ← WAŻNE: wymusza zachowanie aspect ratio */
border: 8px solid var(--accent);
border-radius: var(--radius);
padding: 8px;
overflow: hidden;
}
Ten prosty zabieg zapobiega deformacji grafik, gdy kontener zmienia swój rozmiar, na przykład na urządzeniach mobilnych. To drobiazg, który oszczędza sporo frustracji przy testowaniu responsywności.
Ostatnim elementem układanki jest zmiana sposobu, w jaki osadzamy obrazy w naszych wpisach. Do tej pory używaliśmy standardowej składni Markdown.
Poprzednio (markdown):

Teraz, aby skorzystać z całej magii Hugo Pipes, przełączamy się na nasz nowy shortcode.
Teraz (shortcode):
{{/**<image src="img/image-1.png" alt="Image desc"> */}}
Dzięki tej zmianie, zamiast statycznego linku do pliku, Hugo podstawi w tym miejscu całą strukturę tagu picture lub img z odpowiednimi atrybutami srcset i formatami nowej generacji. Wymaga to chwili przyzwyczajenia przy pisaniu, ale efekt wydajnościowy jest tego warty.
Co się dzieje za kulisami?#
Gdy odpalam build w stylu hugo –minify, cała „magia” dzieje się dlatego, że obrazy przechodzą przez pipeline Hugo Pipes, czyli wbudowane przetwarzanie assetów podczas generowania strony. To nie jest zewnętrzny skrypt ani ręczna robota w edytorze, tylko część procesu budowania, sterowana z poziomu szablonów i shortcode.
- Resize do sensownych wymiarów
Pierwszy krok to zmniejszenie obrazu do rozmiaru, który faktycznie ma sens na stronie, z zachowaniem proporcji. W praktyce oznacza to, że nie serwuję „surowego” 4K, jeśli layout i tak nigdy nie pokaże więcej niż około 1200 px szerokości.
- Konwersja do WebP
Kolejna rzecz to konwersja formatu, na przykład z PNG do WebP, zwykle z ustawioną jakością w okolicach 80. W efekcie dostaję plik znacząco lżejszy, a wizualnie w większości przypadków różnica jest trudna do zauważenia przy normalnym oglądaniu.
- Responsive images
Zamiast jednego pliku generuję kilka wariantów rozmiaru, na przykład 800 px, 1200 px i 1600 px, a potem wystawiam je jako srcset. Dzięki temu przeglądarka może wybrać najlepszą wersję pod realny ekran i DPI urządzenia, zamiast zawsze pobierać „największego potwora”.
- Lazy loading
Na etapie shortcode dorzucam loading=“lazy”, żeby grafiki spoza pierwszego ekranu nie startowały z pobieraniem od razu. To jest prosta zmiana, ale daje realny efekt: użytkownik, który nie przewinie strony, nie płaci transferem za treści, których nie zobaczy.
- Asynchroniczne dekodowanie
Analogicznie ustawiam decoding=“async”, żeby dekodowanie obrazów mniej przeszkadzało w renderowaniu. W praktyce strona ma większą szansę „wstać” szybciej, a obrazy dociągają się w tle wtedy, kiedy przeglądarka ma na to przestrzeń.
Wyniki optymalizacji obrazów#
Liczby mówią same za siebie. Porównajmy rozmiary trzech głównych grafik przed i po wdrożeniu naszego procesu. Zastąpienie formatu PNG nowoczesnym WebP oraz dostosowanie rozdzielczości przyniosło spektakularne rezultaty.
| Plik | Rozmiar przed (PNG) | Rozmiar po (WebP, q80) | Redukcja |
|---|---|---|---|
| Image 1 | 6.83 MB | 155 KB | ~97.7% |
| Image 2 | 5.93 MB | 102 KB | ~98.2% |
| Image 3 | 1.53 MB | 88 KB | 94.3% |
| Favicon | 190 B | 190 B | 0% (bez zmian) |
| SUMA | ~14.29 MB | ~345 KB | ~97.6% |
Dla typowej strony, na której ładujemy te trzy grafiki, zredukowaliśmy transfer z blisko piętnastu megabajtów do zaledwie trzystu pięćdziesięciu kilobajtów. To oszczędność rzędu 98% na każdym odsłonięciu strony. Co najważniejsze, czas ładowania skrócił się z ponad czterech sekund do ułamka sekundy, co jest kluczowe dla doświadczenia użytkownika (UX).
Weryfikacja plików po zbudowaniu#
Jeśli chcecie zobaczyć, co dokładnie wygenerował Hugo, wystarczy uruchomić polecenie budowania strony i zajrzeć do katalogu publicznego.
# Build strony z image processing
hugo --minify
# Sprawdź rozmiary wygenerowanych obrazów
ls -lh public/img/
# Przykładowy output:
# image-1_hu_393c60f826dfd825.webp 1.2M (was 7.16MB PNG)
# image-1_hu_3943bfca655b8414.webp 800K (800px version)
# image-1_hu_f3273b924940ab4b.webp 1.8M (1600px version)
# image-1_hu_a4439ec021e41ca7.png 1.5M (PNG fallback)
W folderze wynikowym zauważycie coś ciekawego. Zamiast jednego pliku, znajdziecie tam kilka wersji tego samego obrazu o różnych rozdzielczościach (na przykład wersje 800px i 1600px) oraz plik awaryjny (fallback).
Zwróćcie też uwagę na nazwy plików. Hugo automatycznie dodaje do nich ciąg losowych znaków, tak zwany hash lub fingerprint. Dzięki temu mechanizmowi przeglądarki wiedzą, kiedy plik się zmienił i należy go pobrać ponownie, co skutecznie rozwiązuje problemy z cache’owaniem (cache busting).
Testowanie i weryfikacja#
Wdrożenie to jedno, ale musimy mieć pewność, że wszystko działa zgodnie z planem. Oto jak w prosty sposób możecie zweryfikować efekty naszej pracy.
- Sprawdź, czy obrazy są generowane
Pierwszy test wykonujemy bezpośrednio w terminalu. Po zbudowaniu strony poleceniem hugo –minify, zaglądamy do katalogu z zasobami.
# Po: hugo --minify
ls -lh resources/_gen/images/img/
# Powinno pokazać:
# - Wiele plików .webp (różne rozmiary)
# - Pliki .png (fallback)
# - Hashes w nazwach (fingerprinting)
Powinniście tam zobaczyć wiele plików. Oprócz oryginałów, będą tam wersje WebP w różnych rozmiarach oraz pliki PNG pełniące rolę zabezpieczenia (fallback). Zwróćcie uwagę na dziwne ciągi znaków w nazwach plików – to fingerprinting, który gwarantuje, że użytkownicy zawsze zobaczą najnowszą wersję grafiki.
- Sprawdź HTML w przeglądarce
Następnie otwieramy naszą stronę w przeglądarce i zaglądamy pod maskę używając DevTools. Znajdźcie element <picture>.
# DevTools → Elements → Znajdź <picture>
# Powinno pokazać:
# <picture class="post-cover">
# <source type="image/webp" srcset="...">
# <img src="..." loading="lazy">
# </picture>
Jeśli wszystko poszło dobrze, zobaczycie tam znaczniki <source> z atrybutem srcset wskazującym na pliki WebP oraz standardowy tag <img> z atrybutem loading=“lazy”. To dowód na to, że przeglądarka otrzymuje komplet instrukcji, jak optymalnie obsłużyć grafikę.
- Test w różnych środowiskach
Warto też sprawdzić zachowanie w różnych przeglądarkach. Chrome i Firefox powinny bez problemu załadować lekkie pliki WebP. Jeśli macie dostęp do starszego sprzętu Apple (Safari poniżej wersji 14), powinniście tam zobaczyć klasyczny plik PNG – to nasz mechanizm awaryjny w działaniu. Na telefonie sprawdźcie natomiast, czy pobierana jest mniejsza wersja obrazu (np. 800px) zamiast pełnej rozdzielczości desktopowej.
Rozwiązywanie problemów#
Na koniec kilka typowych pułapek, na które możecie trafić podczas wdrażania.
Problem: “Image not found”#
Najczęstszy błąd to ten, o którym wspominałem na początku: szukanie obrazów w złym miejscu. Jeśli Hugo krzyczy, że nie widzi pliku, upewnijcie się, że przenieśliście go z folderu static do assets. Pamiętajcie: static jest tylko kopiowany, assets jest przetwarzany.
Problem: Brak nowych obrazów po przebudowie#
Czasami Hugo może nie zauważyć zmian w plikach źródłowych i korzystać ze starej pamięci podręcznej. Jeśli mimo zmian w kodzie nie widzicie nowych wersji grafik, warto wyczyścić cache.
# Clean build
rm -rf resources public
hugo --minify --gc
To wymusi na Hugo ponowne przeliczenie wszystkich zasobów.
Problem: Zniekształcone proporcje#
Jeśli obrazy wydają się rozciągnięte lub ściśnięte, winowajcą jest zazwyczaj CSS. Upewnijcie się, że w waszych stylach dla tagu img znajduje się reguła height: auto. Bez niej przeglądarka może próbować dopasować obraz do sztywnych ram kontenera, ignorując jego naturalne proporcje.
Podsumowanie#
Optymalizacja obrazów to bez wątpienia najbardziej opłacalna pojedyncza zmiana, jaką możecie wprowadzić w swoim projekcie opartym na Hugo. Liczby, które uzyskaliśmy, mówią same za siebie i pokazują skalę problemu, który często ignorujemy.
Wyniki w liczbach#
Zredukowaliśmy całkowity rozmiar przesyłanych danych z niemal 15 MB do zaledwie 345 KB. To spadek o ponad 97%. Czas ładowania skrócił się z ponad czterech sekund do niecałych siedmiuset milisekund. Dla użytkownika mobilnego to różnica między natychmiastowym dostępem do treści a frustrującym oczekiwaniem.
Dlaczego to zadziałało?#
Sekret tkwi w połączeniu kilku mechanizmów. Format WebP zapewnia drastycznie lepszą kompresję niż klasyczne PNG. Skalowanie obrazów do rzeczywistych wymiarów (na przykład 1200 px zamiast oryginalnych 4000 px) eliminuje przesyłanie zbędnych pikseli. Responsywne obrazy i lazy loading sprawiają, że pobieramy tylko to, co jest w danym momencie potrzebne na konkretnym ekranie. A co najlepsze — dzięki Hugo wszystko to dzieje się automatycznie podczas budowania strony.
Wartość biznesowa#
Szybsze ładowanie to nie tylko kwestia technicznej satysfakcji. To bezpośrednie przełożenie na lepsze User Experience i niższy współczynnik odrzuceń (bounce rate). Mniejszy transfer oznacza niższe koszty hostingu, a strona przyjazna urządzeniom mobilnym zyskuje w oczach Google i lepiej pozycjonuje się dzięki dobrym wynikom Core Web Vitals.
Jeśli macie czas na zoptymalizowanie tylko jednej rzeczy w waszym serwisie na Hugo — zacznijcie od obrazów. Dzięki wbudowanym narzędziom Image Processing wdrożenie jest prostsze niż myślicie, a efekt natychmiastowy.
Dodatkowe możliwości Hugo Image Processing#
Hugo oferuje jeszcze więcej funkcji optymalizacji obrazów, których nie testowałem ale warto o nich wiedzieć:
Blur Placeholder (LQIP)#
Hugo może generować miniaturowe, rozmyte wersje obrazków jako placeholder podczas ładowania. więcej
Smart Crop#
Hugo wykrywa twarze i punkty uwagi w obrazie, automatycznie kadrując najważniejszą część. Przydatne dla automatycznego generowania thumbnails i social media cards. więcej
Filtry obrazów#
Hugo wspiera filtry: GaussianBlur, Grayscale, Sepia, Brightness, Contrast, Gamma, ColorBalance, Saturation. więcej
Te funkcje są dostępne w Hugo i mogą być dodane w przyszłości dla jeszcze lepszego user experience.
Źródła i dalsze materiały#
- Hugo Image Processing – oficjalna dokumentacja
- Hugo Resize – dokumentacja funkcji Resize
- WebP - Google Developers – format WebP
- Responsive Images - MDN – srcset i
<picture> - Lazy Loading - web.dev – lazy loading best practices