PageSpeed Insights pokazuje, że strona jest wolna. Nie zawsze pokazuje jednak, dlaczego.
Możesz skompresować obrazy, włączyć cache, zminimalizować CSS i nadal mieć WordPressa, który przy każdym wejściu wykonuje kilkadziesiąt niepotrzebnych zapytań do bazy, czeka na zewnętrzne API albo przetwarza tę samą treść od początku.
Dlatego przy optymalizacji WordPressa nie zaczynamy od próby zdobycia 100 punktów w PageSpeed. Najpierw sprawdzamy, gdzie aplikacja rzeczywiście traci czas.
Poniżej zebrałem 15 rzeczy, które warto sprawdzić w kodzie i konfiguracji WordPressa. To problemy, które regularnie pojawiają się w istniejących stronach i sklepach, a część z nich można znaleźć i poprawić w kilkanaście minut.
Najpierw: PageSpeed Insights nie pokazuje całego obrazu
Zanim zaczniemy grzebać w kodzie, trzeba rozróżnić dwie rzeczy, które PageSpeed Insights pokazuje w jednym raporcie.
Dane laboratoryjne pochodzą z Lighthouse. To test wykonany w kontrolowanych warunkach na emulowanym urządzeniu i połączeniu.
Dane rzeczywistych użytkowników pochodzą z Chrome UX Report i obejmują okres ostatnich 28 dni. To właśnie tutaj znajdziesz Core Web Vitals: LCP, INP i CLS.
Możesz więc mieć 90 punktów w Lighthouse i nadal mieć problem z INP u prawdziwych użytkowników. Możliwa jest również odwrotna sytuacja: wynik laboratoryjny wygląda przeciętnie, ale rzeczywiste dane użytkowników są dobre.
Dlatego liczba w kolorowym kółku nie powinna być głównym celem optymalizacji.
1. Sprawdź, czy nie pobierasz tych samych pól ACF wiele razy
To bardzo prosty przykład, ale dobrze pokazuje sposób myślenia przy optymalizacji backendu.
W szablonie możesz znaleźć coś takiego:
php
$title = get_field('title');
$price = get_field('price');
$image = get_field('image');
Jeżeli potrzebujesz wielu pól tego samego obiektu, warto sprawdzić, czy nie można pobrać ich razem:
php
$fields = get_fields();
$title = $fields['title'];
$price = $fields['price'];
$image = $fields['image'];
Nie chodzi o mechaniczne zastępowanie każdego get_field(). ACF i WordPress korzystają z własnych mechanizmów cache, więc nie każde kolejne wywołanie oznacza nowe zapytanie SQL.
Warto natomiast zwrócić uwagę na kod, który wielokrotnie pobiera i przetwarza te same dane. Szczególnie jeśli dzieje się to wewnątrz pętli.
2. Nie odpytuj zewnętrznego API przy każdym wejściu
Jednym z najłatwiejszych sposobów na bardzo niestabilny TTFB jest uzależnienie renderowania strony od zewnętrznego API.
Przykład:
php
$data = file_get_contents('https://api.example.com/data');
$data = json_decode($data, true);
Każdy użytkownik musi teraz czekać nie tylko na Twój serwer, ale również na odpowiedź drugiego systemu.
Jeżeli API odpowie w 100 ms, może być dobrze. Jeżeli odpowie w dwie sekundy, Twoja strona również czeka. Jeżeli przestanie odpowiadać, sytuacja robi się jeszcze gorsza.
Jeśli dane nie muszą być aktualne co sekundę, zapisz je w cache:
php
$data = get_transient('crm_data');
if ($data === false) {
$response = wp_remote_get('https://api.example.com/data');
if (!is_wp_error($response)) {
$data = json_decode(
wp_remote_retrieve_body($response),
true
);
set_transient(
'crm_data',
$data,
15 * MINUTE_IN_SECONDS
);
}
}
Zewnętrzny system jest wtedy odpytywany raz na określony czas, a nie przy każdym wejściu użytkownika.
Przy bardziej krytycznych integracjach poszedłbym jeszcze dalej: synchronizacja powinna odbywać się w tle, a frontend powinien czytać dane, które znajdują się już lokalnie.
3. Uważaj na operacje na plikach wykonywane przy każdym requeście
Odczytywanie i parsowanie pliku za każdym razem, gdy WordPress generuje stronę, również potrafi niepotrzebnie zwiększać koszt requestu.
php
if (file_exists(get_template_directory() . '/config.json')) {
$config = json_decode(
file_get_contents(
get_template_directory() . '/config.json'
),
true
);
}
Jeżeli te same dane są potrzebne kilka razy podczas jednego requestu, można przynajmniej zatrzymać wynik w pamięci:
php
function get_config() {
static $config = null;
if ($config === null) {
$config = json_decode(
file_get_contents(
get_template_directory() . '/config.json'
),
true
);
}
return $config;
}
Jeżeli konfiguracja zmienia się rzadko, można pójść dalej i wykorzystać cache trwały albo przetworzyć ją wcześniej.
4. Dodaj no_found_rows tam, gdzie nie potrzebujesz paginacji
Własne zapytania WP_Query to jedno z pierwszych miejsc, które sprawdzamy na wolnej stronie.
Typowy fragment wygląda tak:
php
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 6,
]);
Jeżeli wyświetlasz po prostu sześć ostatnich wpisów i nie potrzebujesz informacji o całkowitej liczbie wyników ani paginacji, dodaj:
php
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 6,
'no_found_rows' => true,
]);
WordPress nie musi wtedy wykonywać dodatkowej pracy potrzebnej do policzenia pełnej liczby wyników.
Na pojedynczym zapytaniu różnica może być mała. Jeśli takich komponentów jest kilka, baza jest duża, a strona obsługuje dużo requestów, małe koszty zaczynają się sumować.
5. Jeśli potrzebujesz tylko ID, nie pobieraj całych obiektów
Podobna zasada dotyczy danych zwracanych przez WP_Query.
Jeżeli potrzebujesz wyłącznie identyfikatorów produktów, nie zawsze ma sens pobieranie pełnych obiektów WP_Post.
php
$query = new WP_Query([
'post_type' => 'product',
]);
Możesz ograniczyć wynik:
php
$query = new WP_Query([
'post_type' => 'product',
'fields' => 'ids',
]);
To szczególnie istotne przy większych zbiorach i operacjach wykonywanych w tle.
6. Szukaj posts_per_page ustawionego na -1
posts_per_page => -1 jest wygodne, bo oznacza: pobierz wszystko.
I właśnie dlatego potrafi być niebezpieczne.
php
$query = new WP_Query([
'post_type' => 'product',
'posts_per_page' => -1,
]);
Przy 40 rekordach nic się nie stanie. Przy 40 000 sytuacja wygląda zupełnie inaczej.
Jeżeli nie potrzebujesz całego zbioru jednocześnie, wprowadź limit i paginację:
php
$query = new WP_Query([
'post_type' => 'product',
'posts_per_page' => 12,
]);
W istniejącym projekcie warto po prostu przeszukać kod pod kątem posts_per_page i sprawdzić każde miejsce z wartością -1.
WordPress pozwala bardzo łatwo filtrować wpisy po metadanych:
php
'meta_query' => [
[
'key' => 'price',
'value' => 100,
'compare' => '>',
'type' => 'NUMERIC',
],
]
To wygodne rozwiązanie, ale przy dużych zbiorach wp_postmeta może stać się jednym z najcięższych elementów całej aplikacji.
Jeżeli na metadanych budujesz intensywnie używane filtrowanie, sortowanie albo raportowanie, warto sprawdzić rzeczywiste zapytania w Query Monitorze i przeanalizować je przez EXPLAIN.
sql
EXPLAIN
SELECT ...
FROM wp_posts
INNER JOIN wp_postmeta
ON wp_posts.ID = wp_postmeta.post_id
WHERE ...;
W zależności od rodzaju danych lepszym rozwiązaniem może być taksonomia, dodatkowe indeksy albo osobna tabela zaprojektowana pod konkretny sposób wyszukiwania.
Nie ma sensu przepisywać każdego meta_query. Problem zaczyna się wtedy, gdy traktujemy wp_postmeta jak uniwersalną bazę do dowolnego rodzaju zapytań na dużej liczbie rekordów.
8. Włącz persistent object cache tam, gdzie ma sens
WordPress ma mechanizm Object Cache, ale bez dodatkowego backendu dane nie są zachowywane pomiędzy kolejnymi requestami.
Przy większych serwisach i WooCommerce warto sprawdzić Redis lub Memcached.
Najprościej zacząć od Query Monitora i zobaczyć, czy persistent object cache jest aktywny oraz ile zapytań wykonuje konkretna podstrona.
Redis nie naprawi źle napisanego zapytania ani kodu pobierającego tysiące rekordów. Może jednak bardzo dobrze ograniczyć koszt danych, które WordPress pobiera wielokrotnie.
9. Sprawdź autoload w wp_options
To jedna z rzeczy, które potrafią narastać przez lata razem z instalowaniem i usuwaniem kolejnych wtyczek.
Opcje oznaczone jako autoload są ładowane automatycznie podczas działania WordPressa. Jeżeli trafiają tam duże struktury danych, płacisz za nie przy ogromnej liczbie requestów, nawet jeśli konkretna podstrona ich nie potrzebuje.
Warto zacząć od sprawdzenia największych wpisów:
sql
SELECT
option_name,
LENGTH(option_value) AS size
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY size DESC
LIMIT 50;
Nie wyłączaj jednak autoloadu mechanicznie dla wszystkiego, co jest duże.
Najpierw ustal, do czego należy dana opcja, czy jest nadal używana i jak często aplikacja jej potrzebuje. Dopiero wtedy można zdecydować, czy powinna być ładowana automatycznie.
10. Sprawdź, co korzysta z admin-ajax.php
Starsze wtyczki i motywy bardzo często realizują komunikację z backendem przez:
js
fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
body: data
});
Samo użycie admin-ajax.php nie jest błędem. Problem pojawia się wtedy, gdy frontend odpytuje go często albo wykorzystuje do prostych operacji, które można zrealizować inaczej.
W DevTools otwórz zakładkę Network, wpisz admin-ajax.php w filtrze i używaj strony przez chwilę.
Jeżeli request pojawia się co kilka sekund bez wyraźnej potrzeby albo każda drobna interakcja użytkownika generuje kolejne wywołanie, warto sprawdzić jego źródło.
Dla własnych endpointów można wykorzystać WordPress REST API:
js
fetch('/wp-json/custom/v1/data')
.then(response => response.json())
.then(data => {
console.log(data);
});
Najważniejsza nie jest jednak sama zamiana jednego adresu na drugi. Trzeba sprawdzić, co endpoint wykonuje, jak często jest wywoływany i czy odpowiedź można cache'ować.
11. Cache'uj wynik ciężkich obliczeń, a nie tylko strony
Cache strony jest świetny, dopóki użytkownik może dostać gotowy HTML.
Nie rozwiązuje jednak każdego przypadku. WooCommerce, zalogowani użytkownicy, API i dynamiczne fragmenty aplikacji nadal mogą wykonywać kosztowne operacje.
Załóżmy, że przygotowanie danych wymaga pobrania produktów, przeliczenia ich i posortowania:
php
usort($products, function ($a, $b) {
return $a['price'] $b['price'];
});
Jeżeli wynik nie zmienia się przy każdym requeście, możesz zapisać gotowe dane:
php
$products = get_transient('sorted_products');
if ($products === false) {
$products = get_products();
usort($products, function ($a, $b) {
return $a['price'] $b['price'];
});
set_transient(
'sorted_products',
$products,
HOUR_IN_SECONDS
);
}
Zasada jest prosta: jeśli wykonujesz kosztowną operację setki razy na tych samych danych, sprawdź, czy naprawdę musi być wykonywana setki razy.
12. Wyprowadź WP-Cron z requestów użytkowników
Domyślny WP-Cron nie jest klasycznym cronem systemowym. WordPress sprawdza zadania przy ruchu na stronie.
Przy prostym blogu często nie ma to większego znaczenia. W WooCommerce albo rozbudowanym systemie liczba zadań potrafi jednak znacząco wzrosnąć.
Efektem mogą być requesty, które raz odpowiadają szybko, a innym razem nagle wykonują dodatkową pracę.
Możesz wyłączyć standardowe uruchamianie WP-Cron:
php
define('DISABLE_WP_CRON', true);
A następnie uruchamiać zadania z prawdziwego schedulera:
bash
*/5 * * * * wp cron event run --due-now --path=/var/www/html > /dev/null 2>&1
Jeżeli WordPress działa w kontenerach, cron może mieć własny worker:
yaml
cron:
image: wordpress:cli
volumes:
- ./:/var/www/html
entrypoint: >
sh -c "while true; do
wp cron event run --due-now --path=/var/www/html;
sleep 300;
done"
Dzięki temu wykonanie zadań nie zależy od tego, czy ktoś właśnie otworzył stronę, a logowanie i diagnozowanie problemów jest prostsze.
13. Nie parsuj całego HTML przy każdym wyświetleniu strony
To jeden z ciekawszych problemów, bo na pierwszy rzut oka kod może wyglądać całkiem niewinnie.
php
add_filter('the_content', function ($content) {
$dom = new DOMDocument();
@$dom->loadHTML($content);
// manipulacje na HTML
return $dom->saveHTML();
});
Teraz każde wyświetlenie wpisu może uruchamiać parser HTML, budować strukturę DOM, modyfikować ją i ponownie serializować.
Jeżeli efekt zależy wyłącznie od treści wpisu, znacznie lepszym miejscem na ciężką pracę może być moment zapisu:
php
add_action('save_post', function ($post_id) {
$content = get_post_field(
'post_content',
$post_id
);
$dom = new DOMDocument();
@$dom->loadHTML($content);
// manipulacje na HTML
$processed = $dom->saveHTML();
update_post_meta(
$post_id,
'_processed_content',
$processed
);
});
Frontend pobiera wtedy gotowy wynik:
php
echo get_post_meta(
get_the_ID(),
'_processed_content',
true
);
To jedna z ważniejszych zasad optymalizacji aplikacji: jeżeli coś można policzyć raz podczas zapisu, nie licz tego ponownie przy każdym odczycie.
14. Sprawdź ciężką logikę podpiętą do globalnych hooków
init, wp i podobne hooki są wygodne, ale trzeba pamiętać, jak często się wykonują.
Taki kod:
php
add_action('init', function () {
$data = expensive_operation();
});
może wykonywać ciężką operację przy ogromnej liczbie requestów, mimo że jej wynik potrzebny jest tylko w jednym miejscu.
Najpierw ogranicz zakres wykonania:
php
add_action('wp', function () {
if (!is_page('oferta')) {
return;
}
expensive_operation();
});
A jeszcze lepiej sprawdź, czy operacja musi być wykonywana podczas requestu użytkownika.
Jeżeli dane można przygotować podczas zapisu, przez cron albo po zmianie konkretnej opcji, frontend powinien dostać już gotowy wynik.
15. Nie ładuj CSS i JavaScriptu wszędzie
To klasyczny problem rozbudowanych instalacji WordPressa.
Formularz jest tylko na stronie kontaktowej, ale jego CSS i JavaScript trafiają na każdą podstronę. Slider działa na stronie głównej, ale jego biblioteka jest obecna również na blogu. Skrypt WooCommerce trafia tam, gdzie nie ma żadnej funkcji sklepowej.
Najpierw otwórz DevTools → Network i Query Monitor → Scripts & Styles.
Sprawdź, które zasoby pochodzą z wtyczek i czy rzeczywiście są potrzebne na aktualnej podstronie.
Dla własnego projektu można ładować assety warunkowo albo usuwać niepotrzebne z kolejki:
php
add_action('wp_enqueue_scripts', function () {
if (!is_page('kontakt')) {
wp_dequeue_style('contact-form-7');
wp_dequeue_script('contact-form-7');
}
}, 100);
Nie kopiuj jednak gotowej listy wp_dequeue_* z internetu.
Sprawdź zależności na swojej stronie i przetestuj funkcje po każdej zmianie. Wtyczka może wykorzystywać skrypt również w miejscu, którego na pierwszy rzut oka nie widać.
A gdzie obrazy, WebP, minifikacja i lazy loading?
Są ważne, ale to zwykle właśnie od nich zaczynają wszystkie poradniki.
Jeżeli obraz hero waży 4 MB, oczywiście trzeba go poprawić. Jeśli strona ładuje dziesięć odmian fontu, również warto się tym zająć.
Problem w tym, że można doprowadzić frontend do bardzo dobrego stanu i nadal czekać sekundę lub dwie, zanim serwer zacznie wysyłać HTML.
Dlatego patrzymy również na TTFB.
Jeżeli jest wysoki, sprawdzamy między innymi page cache, wersję PHP, zapytania do bazy, object cache, zewnętrzne API, cron oraz kod wykonywany przy każdym requeście.
Dopiero potem optymalizujemy to, co przeglądarka robi z otrzymanym dokumentem.
Sprawdź obraz LCP, zanim zainstalujesz kolejną wtyczkę do lazy loadingu
WordPress ma już własne mechanizmy optymalizujące ładowanie obrazów.
Problem pojawia się, kiedy dodatkowa wtyczka do lazy loadingu próbuje zrobić to jeszcze raz i opóźnia obraz, który użytkownik powinien zobaczyć jako pierwszy.
Otwórz kod wygenerowanej strony i znajdź główny obraz w pierwszej sekcji.
Jeżeli zobaczysz coś podobnego:
html
<img
src="hero.webp"
loading="lazy"
alt="..."
>
sprawdź, czy właśnie ten obraz nie jest Twoim LCP.
Dla obrazu znajdującego się od razu w pierwszym widoku opóźnianie pobrania może pogorszyć wynik zamiast go poprawić.
W odpowiednim przypadku przeglądarka może dostać informację o wysokim priorytecie:
html
<img
src="hero.webp"
fetchpriority="high"
alt="..."
>
Nie oznacza to, że należy dodać fetchpriority="high" do wszystkich obrazów. Priorytet przestaje mieć sens, jeżeli wszystko staje się priorytetem.
Najtrudniejszy problem WooCommerce często zaczyna się po załadowaniu strony
Sklep może otworzyć się szybko, a mimo tego sprawiać wrażenie ciężkiego.
Użytkownik wybiera wariant produktu, otwiera mini-koszyk, filtruje kategorię albo zmienia liczbę produktów i dopiero wtedy interfejs zaczyna się zacinać.
W tym momencie jednocześnie mogą działać skrypty motywu, WooCommerce, kilku dodatków, analityki, reklam, czatu i narzędzia do testów A/B.
To właśnie dlatego nie można oceniać wydajności sklepu wyłącznie na podstawie pierwszego załadowania strony.
W rzeczywistych danych odpowiada za to między innymi INP. Dobry wynik to maksymalnie 200 ms na 75. percentylu rzeczywistych wizyt.
Jeżeli PageSpeed pokazuje 90 punktów, ale użytkownik po kliknięciu „Dodaj do koszyka” przez sekundę nie widzi reakcji, problem nadal istnieje.
Jak diagnozujemy wolnego WordPressa
Nie zmieniamy piętnastu rzeczy jednocześnie, bo później nie wiadomo, która z nich faktycznie pomogła.
Zaczynamy od pomiaru TTFB i danych Core Web Vitals. Następnie sprawdzamy Query Monitor, liczbę i czas zapytań SQL, object cache, requesty HTTP wykonywane przez backend oraz zasoby ładowane przez przeglądarkę.
Przy podejrzeniu konkretnego fragmentu kodu używamy profilera. Przy wolnych zapytaniach sprawdzamy SQL i EXPLAIN. Przy problemach po stronie przeglądarki korzystamy z Performance i Network w DevTools.
Dopiero po znalezieniu wąskiego gardła zmieniamy kod.
Po zmianie wykonujemy ten sam pomiar ponownie.
To ważne, ponieważ „optymalizacja” wykonana bez pomiaru bardzo łatwo kończy się kolejną wtyczką, dodatkową warstwą cache i większą złożonością bez zauważalnej poprawy.
W jakiej kolejności optymalizować WordPressa?
Jeżeli mielibyśmy sprowadzić cały proces do jednej kolejności, wyglądałaby tak:
- Sprawdź rzeczywiste Core Web Vitals i TTFB.
- Oddziel czas backendu od czasu renderowania w przeglądarce.
- Sprawdź page cache i konfigurację serwera.
- Znajdź wolne oraz powtarzalne zapytania do bazy.
- Sprawdź zewnętrzne API i inne operacje blokujące request.
- Sprawdź WP-Cron i ciężką logikę wykonywaną globalnie.
- Sprawdź autoload oraz persistent object cache.
- Usuń niepotrzebny JavaScript i CSS z podstron, które go nie wykorzystują.
- Popraw LCP, fonty i stabilność układu.
- Przetestuj prawdziwe interakcje, szczególnie w WooCommerce.
- Zmierz ponownie.
Dopiero na końcu zastanawiałbym się, czy 96 punktów w Lighthouse koniecznie trzeba zamienić na 100.
100 punktów w PageSpeed nie jest celem
PageSpeed Insights jest bardzo dobrym narzędziem diagnostycznym. Problem zaczyna się wtedy, gdy wynik staje się celem samym w sobie.
Strona istnieje dla użytkownika, nie dla Lighthouse.
Jeżeli sklep szybko pokazuje produkt, natychmiast reaguje na wybór wariantu, sprawnie dodaje produkt do koszyka i nie przesuwa przycisku w momencie kliknięcia, to są rzeczywiste korzyści z optymalizacji.
Jeżeli udało się przy okazji zdobyć kilka dodatkowych punktów w PageSpeed, tym lepiej.
Nie warto natomiast komplikować projektu tylko po to, żeby zamienić 97 na 100.
Co robimy, kiedy przejmujemy wolną stronę
Przy utrzymaniu i rozwoju WordPressa nie zaczynamy od instalowania kolejnej wtyczki optymalizacyjnej.
Najpierw ustalamy, gdzie rzeczywiście tracony jest czas: na serwerze, w bazie danych, w kodzie PHP, na zewnętrznym API czy już po stronie przeglądarki.
Przy WooCommerce dodatkowo sprawdzamy najważniejsze interakcje zakupowe, ponieważ szybkie otwarcie strony nie oznacza jeszcze szybkiego sklepu.
Następnie przygotowujemy listę zmian w kolejności od tych, które mogą dać największy efekt, do optymalizacji kosmetycznych.
Jeżeli chcesz sprawdzić, dlaczego Twój WordPress albo WooCommerce działa wolno, napisz do nas. Możemy zacząć od analizy i wskazać konkretne miejsca, które warto poprawić.