WP_Query to generator SQL stojący za każdym żądaniem na froncie: archiwami, pojedynczymi wpisami, endpointami REST i pętlami sklepu WooCommerce. Gdy strona działa wolno, przyczyną jest zwykle jedna z trzech rzeczy: meta_query na nieindeksowanym meta_key, nieograniczone posts_per_page albo SQL_CALC_FOUND_ROWS uruchamiane przy każdym wczytaniu strony, choć nigdzie nie wyświetla się łączna liczba wyników.
Rząd wielkości z prawdziwego sklepu WooCommerce z 50 000 produktów: meta_query na _stock_status bez indeksu na (meta_key, post_id) trwa 1,5-3 sekundy. Po dodaniu indeksu złożonego to samo zapytanie spada do 50-200 ms. Zapytanie jest identyczne, zmienił się tylko indeks.
Reszta tego przewodnika to odruchy, które pozwalają uniknąć tych trzech błędów i wyjść z nich, gdy odziedziczony motyw ma je już wbudowane. Projektowanie zapytań po taksonomiach i metadanych, indeksy złożone oraz cache zapytań dodany w WordPress 6.1 mają osobny artykuł: zaawansowane WP_Query, taksonomie, metadane i wydajność. Oficjalna dokumentacja: WP_Query, pre_get_posts i kod źródłowy class-wp-query.php.
Jak WP_Query buduje i wykonuje zapytanie
Żeby dobrze panować nad WP_Query, trzeba wiedzieć, co się dzieje przy tworzeniu obiektu. WordPress wykonuje kolejno:
- Parsowanie argumentów: zamienia tablicę argumentów na ujednolicony format.
- Generowanie SQL: buduje złożone zapytanie SQL SELECT.
- Wykonanie zapytania: wysyła żądanie do bazy danych.
- Wypełnienie obiektu: zapełnia obiekt
WP_Queryobiektami wpisów i metadanymi.
Właściwości obiektu WP_Query
Po wywołaniu $query = new WP_Query($args) nie dostajesz zwykłej tablicy wpisów, tylko duży obiekt z kilkoma kluczowymi właściwościami:
$query->posts: tablica obiektówWP_Post.$query->post_count: liczba wpisów wyświetlanych na bieżącej stronie.$query->found_posts: łączna liczba wpisów spełniających kryteria (niezależnie od paginacji).$query->max_num_pages: łączna liczba stron wyników.$query->query_vars: argumenty, których WordPress faktycznie użył do wykonania zapytania.
Główne zapytanie a dodatkowe WP_Query
Główne zapytanie ($wp_the_query, dostępne jako globalne $wp_query) powstaje, zanim załaduje się szablon. To ono decyduje, który szablon wybierze WordPress (is_home(), is_archive(), is_singular() i pozostałe), oraz które wpisy pokaże domyślna pętla przez have_posts() i the_post(). Pętla dodatkowa to osobna instancja WP_Query: nie zmienia głównego zapytania i nie służy do naprawiania filtrów archiwum po fakcie. Jeśli chcesz zmienić to, co pokazuje archiwum, użyj pre_get_posts. Jeśli potrzebujesz dodatkowej treści obok głównej listy, użyj new WP_Query().
1. Pętla główna (kontekst globalny)
To pętla uruchamiana przez żądanie URL. WordPress sam tworzy zapytanie, ty tylko po nim iterujesz:
if ( have_posts() ) :
while ( have_posts() ) : the_post();
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
endif;2. Pętla dodatkowa (własne zapytania)
Służy do podobnych wpisów, widżetów z najnowszymi aktualnościami i każdej sekcji, która pobiera listę poza głównym zapytaniem. W praktyce spotyka się trzy sposoby jej tworzenia i nie są one wymienne:
new WP_Query( $args ): pełny obiekt, udostępniafound_postsimax_num_pages, obsługuje iterację przezthe_post(). Używaj go, gdy potrzebujesz paginacji albo pełnego dostępu do tagów szablonu.get_posts( $args ): cienka nakładka zwracająca tablicę obiektówWP_Post. Domyślnie ma'suppress_filters' => truei zawsze ustawia'no_found_rows' => trueoraz'ignore_sticky_posts' => true, więc jest szybsza, ale pomija filtryposts_*. Nadaje się do krótkich list o stałej długości w pasku bocznym.query_posts(): działa destrukcyjnie. Podmienia globalne$wp_queryna nowe zapytanie, psuje warunkiis_*()w dalszej części strony i uruchamia drugie główne zapytanie na tym, które WordPress już wykonał. Dokumentacja funkcji mówi, że nie jest przeznaczona dla wtyczek ani motywów. Nie używaj jej. Jeśli znajdziesz ją w odziedziczonym motywie, zastąp jąpre_get_postsdla głównego zapytania inew WP_Querydla wszystkiego innego.
$args = [
'post_type' => 'post',
'posts_per_page' => 5,
'no_found_rows' => true, // KRYTYCZNE DLA WYDAJNOŚCI
];
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
// Wyświetl tytuł, zajawkę itd.
}
wp_reset_postdata(); // OBOWIĄZKOWE, przywraca globalny obiekt $post
}Jak zmienić główne zapytanie przez pre_get_posts
Dokumentacja pre_get_posts opisuje ten hook jako uruchamiany po utworzeniu obiektu zmiennych zapytania, ale przed faktycznym wykonaniem zapytania. Właśnie o ten moment chodzi: zmieniasz argumenty raz, a WordPress nadal wykonuje jedno zapytanie, zamiast głównego zapytania i drugiego WP_Query na dokładkę.
add_action(
'pre_get_posts',
static function ( WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_home() ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'category__not_in', [ 17 ] );
}
if ( $query->is_post_type_archive( 'case' ) ) {
$query->set( 'orderby', 'menu_order title' );
$query->set( 'order', 'ASC' );
}
}
);Trzy zasady z dokumentacji i z kodu rdzenia:
- Warunki wywołuj jako metody przekazanego obiektu (
$query->is_home()), a nie jako funkcje globalne. Globalneis_main_query()czyta$wp_query, a nie filtrowane zapytanie. W rdzeniuWP_Query::is_main_query()to jedno porównanie:$wp_the_query === $this. - Zabezpiecz callback przez
is_admin()i$query->is_main_query(). Bez obu warunków zmienisz też tabele list w panelu, kanały RSS, żądania REST i każdą dodatkową pętlę na stronie. is_front_page()nie działa wewnątrz tego hooka, bo zapytanie nie jest jeszcze przygotowane.is_home()działa. Przy statycznej stronie głównej sprawdzaj zmienne zapytania.
Argumenty zmieniaj przez $query->set() i nigdy nie wywołuj w callbacku query_posts() ani drugiego new WP_Query().
Co robią the_post() i have_posts() wewnątrz pętli
Lektura class-wp-query.php wyjaśnia kilka błędów, które w szablonach wyglądają na losowe.
- Przy pierwszym wywołaniu
the_post()rdzeń ładuje do cache dane autorów dla całego zestawu wyników i uruchamia akcjęloop_start. Jeśli zapytanie nie zwróciło pełnych obiektów wpisów, w tym momencie ładuje też cache wpisów, metadanych i termów (chyba żeupdate_post_meta_cachealboupdate_post_term_cachema wartośćfalse). - Przy
'fields' => 'ids'obecny rdzeń i tak zamienia każde ID na pełny obiekt wpisu przezget_post()wewnątrzthe_post(). Pętla działa, ale lista ID kosztuje wtedy tyle samo co pełne zapytanie. setup_postdata()ustawia więcej zmiennych globalnych niż$post:$id,$authordata,$currentday,$currentmonth,$page,$pages,$multipage,$morei$numpages, a potem uruchamia akcjęthe_post. Niezresetowana pętla dodatkowa zostawia je wszystkie wskazujące na ostatni podobny wpis. Dlatego box autora albo wpis dzielony na strony (<!--nextpage-->) psuje się po bloku podobnych wpisów.- Gdy
have_posts()wyczerpie wpisy, uruchamialoop_endi przewija zapytanie na początek. Jeśli zapytanie nic nie znalazło, zamiast tego uruchamialoop_no_results.
Jak używać wp_reset_postdata po pętli dodatkowej
Wywołanie the_post() na dodatkowym zapytaniu podmienia globalne $post. Od tej chwili tagi szablonu, takie jak the_title() czy get_permalink(), działają tak, jakby nadal były na ostatnim wpisie tej pętli, dopóki po niej nie wywołasz wp_reset_postdata().
wp_reset_postdata czy wp_reset_query
W wp-includes/query.php różnica to dwie linie. wp_reset_postdata() wywołuje $wp_query->reset_postdata(), które przywraca globalne $post i wymienione wyżej zmienne z głównego zapytania. wp_reset_query() najpierw wstawia $wp_the_query z powrotem do $wp_query, a potem wywołuje wp_reset_postdata(). wp_reset_query() jest potrzebne tylko po query_posts(), a właściwą poprawką jest tam usunięcie query_posts().
Gdy iterujesz przez foreach po tablicy z get_posts(), tagi szablonu nie wiedzą o wpisie, dopóki nie wywołasz setup_postdata( $post ). Po pętli resetujesz tak samo:
global $post;
$items = get_posts( [ 'numberposts' => 3 ] );
foreach ( $items as $post ) {
setup_postdata( $post );
the_title( '<li>', '</li>' );
}
wp_reset_postdata();setup_postdata() sama nie przypisuje globalnego $post, dlatego zmienną pętli jest tu właśnie globalne $post.
Najczęstsze błędy wydajności w WP_Query
1. Dlaczego posts_per_page => -1 wyczerpuje pamięć
-1 każe WP_Query załadować do pamięci każdy pasujący wiersz jako pełny obiekt WP_Post, a potem załadować cache metadanych i termów dla wszystkich (w rdzeniu update_postmeta_cache() i update_object_term_cache()). Przy własnym typie wpisu, który w dwa lata urósł z 50 do 5000 pozycji, ten sam kod, który działał 80 ms, alokuje teraz ponad 200 MB pamięci PHP i przebija limit memory_limit na hostingu współdzielonym.
Poprawka to twardy limit dopasowany do tego, co faktycznie się wyświetla:
- Widżet w pasku bocznym z 5 pozycjami dostaje
'posts_per_page' => 5. - Generator mapy strony, który naprawdę potrzebuje wszystkich wierszy, powinien pobierać je partiami przez
pagedw pętli, a nie przez-1. 200 ID na partię to bezpieczny pułap na typowych serwerach. - Wewnętrzne narzędzia eksportu, w których obciążenie pamięci jest akceptowalne, i tak powinny używać
'fields' => 'ids', żeby nie budować pełnych obiektów wpisów.
2. Kiedy pominąć SQL_CALC_FOUND_ROWS przez no_found_rows
Bez no_found_rows, i zawsze gdy zapytanie ma LIMIT, WP_Query dodaje do SELECT SQL_CALC_FOUND_ROWS, a potem wykonuje SELECT FOUND_ROWS() (przez filtr found_posts_query). MySQL musi liczyć dalej po osiągnięciu LIMIT, co na tabeli wp_posts z 200 000 wierszy z JOIN-em z tax_query dodaje zwykle 80-250 ms na żądanie, zależnie od stanu cache.
Jeśli strona nie pokazuje “Strona 3 z 47” ani łącznej liczby, ustaw 'no_found_rows' => true. Następne pytanie brzmi zawsze: “ale chcę paginację z linkami dalej/wstecz”. Działają bez found_posts, o ile sprawdzasz have_posts() na zapytaniu pobierającym o jeden wiersz więcej (np. prosisz o posts_per_page + 1, wyświetlasz posts_per_page, a dodatkowy wiersz sygnalizuje, że istnieje następna strona).
W archiwach z paginacją, które naprawdę pokazują sumę (“Wyświetlono 1-10 z 4200 artykułów”), zostaw liczenie, ale zapisz wynik w transiencie na 5 minut z kluczem opartym na sygnaturze filtrów. Liczba rzadko zmienia się w ciągu pięciu minut, a skan z JOIN-em nie musi powtarzać się przy każdym wczytaniu strony.
3. Sortowanie losowe (orderby => rand)
ORDER BY RAND() zmusza MySQL do przypisania losowej wartości każdemu pasującemu wierszowi i posortowania całego zbioru, zwykle w tabeli tymczasowej, zanim zadziała LIMIT. Rdzeń też odmawia cache’owania: cache zapytań z WordPress 6.1 pomija każde żądanie, którego ORDER BY zawiera RAND(. Poprawka inżynierska:
- Pobierz ID ostatnich 50 wpisów.
- Wylosuj 5 ID w PHP.
- Wykonaj drugie zapytanie z
post__in => $random_ids.
4. meta_query na nieindeksowanym meta_key
To najczęstsza pojedyncza przyczyna wolnych stron archiwów w starszych instalacjach WooCommerce. schema.php w rdzeniu tworzy wp_postmeta z dwoma indeksami jednokolumnowymi, na post_id i na prefiksie meta_key, bez indeksu złożonego. Indeks na meta_key ma za niską kardynalność, żeby pomóc, gdy WordPress filtruje po meta_key = '_stock_status' AND meta_value = 'instock': MySQL i tak musi przeskanować każdy wiersz z tym kluczem.
Historia z wdrożenia: polski sklep WooCommerce z 50 000 produktów. Strona archiwum sklepu podpinała się pod pre_get_posts, żeby dodać meta_query filtrujące po _stock_status i własnym polu _brand. P95 TTFB strony sklepu wynosił 4,1 sekundy. SAVEQUERIES pokazało, że 80% czasu zajmuje jeden JOIN z wp_postmeta. Poprawka:
ALTER TABLE wp_postmeta
ADD INDEX idx_meta_key_post_id (meta_key(32), post_id);Po dodaniu indeksu to samo zapytanie spadło do 180 ms. Nie zmieniła się ani linia PHP. Dla wartości, które biorą udział w prawdziwych porównaniach zakresowych ('compare' => '>', 'compare' => 'BETWEEN'), potrzebujesz też indeksu na meta_value, zwykle prefiksowego, bo kolumna ma typ LONGTEXT.
Gdy zbiór wartości jest zamknięty (rozmiary, kolory, slugi marek), w ogóle nie używaj meta_query. Zarejestruj własną taksonomię. wp_term_relationships ma klucz główny na (object_id, term_taxonomy_id) i osobny indeks na term_taxonomy_id, więc ten sam filtr, który jako meta_query trwał 1,8 s, jako tax_query wykonuje się w 30-60 ms.
5. Klauzule tax_query sumują się przy dużej skali
WP_Tax_Query najpierw zamienia twoje termy na wartości term_taxonomy_id, a potem każda klauzula IN dodaje jeden LEFT JOIN z wp_term_relationships. Klauzule AND stają się skorelowanym podzapytaniem z COUNT, NOT IN podzapytaniem, a tylko EXISTS i NOT EXISTS sięgają do wp_term_taxonomy. Każda kolejna taksonomia w tym samym zapytaniu to następny JOIN albo podzapytanie, które MySQL musi połączyć, więc najpierw boli to na stronach z filtrami fasetowymi składającymi sześć taksonomii (rozmiar, kolor, marka, sezon, materiał, przedział cenowy). Masz trzy wyjścia: zdenormalizować dane do jednej taksonomii fasetowej, przejść na FacetWP albo SearchWP, które indeksują poza tabelami rdzenia, albo przenieść fasety do wyszukiwarki (Elasticsearch przez ElasticPress albo Algolia dla katalogów tylko do odczytu).
Przykłady WP_Query z tax_query i date_query
1. Relacje w tax_query
Zapytanie o kilka kategorii lub tagów wymaga argumentu tax_query. Parametr relation steruje logiką AND lub OR.
$args = [
'post_type' => 'product',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'color',
'field' => 'slug',
'terms' => [ 'red', 'blue' ],
'operator' => 'IN',
],
[
'taxonomy' => 'size',
'field' => 'slug',
'terms' => [ 'large' ],
],
],
];2. Zapytania po dacie (dynamiczny zakres)
Natywne zapytania po dacie są elastyczne i wydajne. Używaj ich zamiast ręcznego filtrowania w SQL.
$args = [
'date_query' => [
[
'after' => 'January 1st, 2025',
'before' => [
'year' => 2026,
'month' => 12,
'day' => 31,
],
'inclusive' => true,
],
],
];Trzy krótkie argumenty na co dzień
Wykluczenie bieżącego wpisu, np. w sekcji podobnych wpisów, żeby nie pokazywać tekstu, który użytkownik właśnie czyta:
$args = [
'post__not_in' => [ get_the_ID() ],
];Kilka typów wpisów w jednym zapytaniu:
$args = [
'post_type' => [ 'post', 'page', 'product' ],
];Wpisy z ostatniego tygodnia, z relatywną datą w date_query:
$args = [
'date_query' => [
[
'after' => '1 week ago',
],
],
];Paginacja we własnej pętli WP_Query zwraca 404
Jeden z najczęstszych problemów brzmi: “Paginacja na mojej stronie zwraca 404”. Dlaczego tak się dzieje: przy własnym WP_Query WordPress nie wie, na której stronie jesteś. Adresy archiwów (/page/2/) ustawiają zmienną zapytania paged, a statyczna strona główna ustawia zamiast niej page.
Rozwiązanie:
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
if ( is_front_page() ) {
$paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
}
$args = [
'paged' => $paged,
'posts_per_page' => 10,
// ...
];Nie łącz offset z paged. Argument offset nadpisuje obliczanie strony, więc każda strona zwraca ten sam wycinek. Jeśli potrzebujesz układu “jeden wyróżniony wpis, a pod nim lista z paginacją”, wykonaj dwa zapytania albo sam obliczaj offset dla każdej strony.
Jak cache’ować wyniki WP_Query
W 2026 roku strona z dużym ruchem rzadko powinna sięgać do bazy po statyczne listy. Od WordPress 6.1 rdzeń sam cache’uje listę ID powtarzanych identycznych zapytań w cache obiektowym (cache_results), szczegóły są w przewodniku po zaawansowanym WP_Query, do którego link jest na początku. Poniższe wzorce dotyczą tego, czego ten cache nie obejmuje.
1. Transients API
Przy złożonym zapytaniu (np. lokalnej liście “Popularne oferty pracy”) zapisz ID w transiencie na godzinę. Nie zapisuj całego obiektu WP_Query: serializuje on każdy wpis i stan zapytania, a kopia z cache nie zachowuje się już jak świeże zapytanie.
$cache_key = 'home_trending_jobs_v1';
$ids = get_transient( $cache_key );
if ( false === $ids ) {
$q = new WP_Query( [
/* Complex Args */
'fields' => 'ids',
'no_found_rows' => true,
] );
$ids = $q->posts;
set_transient( $cache_key, $ids, HOUR_IN_SECONDS );
}
$trending = new WP_Query( [
'post__in' => $ids ?: [ 0 ],
'orderby' => 'post__in',
'no_found_rows' => true,
] );Usuwaj transient na save_post dla typu wpisu, który lista pokazuje. Bez trwałego cache obiektowego transienty trafiają do wp_options.
2. Wyłączenie ładowania cache dla jednego zapytania
Po pobraniu ID wpisów WP_Query wywołuje update_postmeta_cache() i update_object_term_cache(), żeby hurtowo załadować wszystkie wiersze metadanych i termów tych wpisów do cache obiektowego. To dobre ustawienie domyślne dla zwykłej pętli w szablonie, która za chwilę wywoła get_post_meta() i get_the_terms(). To marnotrawstwo w pętli, która wyświetla tylko tytuł i link.
$args = [
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'no_found_rows' => true,
'fields' => 'ids',
];W archiwum 50 wpisów, gdzie każdy ma 30 wierszy metadanych, pominięcie ładowania cache metadanych oszczędza odczyt i zapis w cache 1500 wierszy w jednym żądaniu. Razem z 'fields' => 'ids' w ogóle nie budujesz obiektów WP_Post. Przydaje się to w generatorach map strony, listach ID podobnych wpisów w pasku bocznym i widżetach kokpitu, pod warunkiem że czytasz bezpośrednio $q->posts i nie puszczasz po nich the_post().
Kiedy WP_Query dzieli zapytanie na ID i obiekty
Rdzeń nie zawsze pobiera pełne wiersze jednym SELECT-em. W WP_Query::get_posts(), gdy SQL nie został zmieniony przez filtry posts_* i albo działa zewnętrzny cache obiektowy, albo zapytanie ma LIMIT z posts_per_page poniżej 500, WordPress najpierw wybiera same ID, a potem uzupełnia obiekty wpisów, z cache obiektowego tam, gdzie już się znajdują. Filtr split_the_query pozwala to włączyć lub wyłączyć.
Dwie konsekwencje dla debugowania:
- SQL w Query Monitor może wyglądać jak
SELECT wp_posts.ID ..., a po nim osobne pobranie brakujących wpisów. To zachowanie rdzenia, nie błąd wtyczki. - Każda wtyczka, która zmienia SQL przez filtry klauzul
posts_*, wyłącza ten podział. Wtyczka zmieniająca wybierane pola (posts_fields,posts_fields_request,posts_clauses_request) wyłącza też dla tego żądania cache zapytań z 6.1.
Istnieje też pełne obejście bazy: filtr posts_pre_query działa przed wywołaniem bazy danych i jeśli zwróci tablicę, WordPress w ogóle pomija SQL. Wtyczki wyszukiwania używają go, żeby odpowiadać na WP_Query z zewnętrznego indeksu. Jeśli Query Monitor pokazuje WP_Query z wynikami i bez pasującego SQL, szukaj callbacku na posts_pre_query.
Jak zmienić wyniki WP_Query w REST API
Jeśli budujesz frontend w React na WordPressie headless, nie używasz WP_Query bezpośrednio w JS, ale REST API korzysta z niego po stronie serwera.
Filtrowanie argumentów zapytania REST API
Filtr rest_{post_type}_query pozwala zmienić sposób, w jaki API odpytuje bazę na podstawie parametrów URL. Najpierw sprawdź endpoint: /wp/v2/posts przyjmuje już natywny parametr exclude, więc własny parametr ma sens tylko dla logiki, której rdzeń nie obsługuje.
add_filter( 'rest_post_query', function( $args, $request ) {
$exclude = $request->get_param( 'exclude_ids' );
if ( ! empty( $exclude ) ) {
$args['post__not_in'] = wp_parse_id_list( $exclude );
}
return $args;
}, 10, 2 );wp_parse_id_list() zamienia ciąg rozdzielony przecinkami na tablicę dodatnich liczb całkowitych, więc spreparowany parametr nie wstrzyknie do zapytania niczego innego.
Jak debugować wolne zapytania WP_Query
Cztery narzędzia, które naprawdę dają odpowiedź:
- Query Monitor (John Blackbourn). Instaluj na stagingu, nigdy na produkcji. Panel Queries grupuje zapytania według komponentu, więc od razu widać, czy 2-sekundowy TTFB pochodzi z motywu, z rdzenia WooCommerce, czy z zewnętrznej wtyczki podpiętej pod
pre_get_posts. Zakładka zduplikowanych zapytań wyłapuje pętle N+1, w których tag szablonu wewnątrzthe_post()odpytuje bazę osobno dla każdego wiersza. SAVEQUERIES. Ustawdefine( 'SAVEQUERIES', true );wwp-config.phpwyłącznie na stagingu, bo każde zapytanie żądania zostaje w pamięci. WordPress wypełnia wtedy$wpdb->queries; każdy wpis zawiera SQL, czas wykonania, stos wywołań, czas startu i opcjonalne dane własne (wpdb::log_query()w rdzeniu).error_log( print_r( $wpdb->queries, true ) )nashutdowndaje kanoniczny zapis tego, co się wykonało i ile trwało.$query->requestplusEXPLAIN. Ponew WP_Query()polecenieecho $query->request;wypisze SQL, który WordPress faktycznie wygenerował. Uruchom ten ciąg zEXPLAINw TablePlus albo przezmysql -e. Szukajtype: ALL(pełny skan tabeli),Using filesortalboUsing temporary: to wiersze, które potrzebują indeksu.- WP-CLI.
wp db query --skip-column-names "EXPLAIN $SQL"uruchamiaEXPLAINna żywej bazie bez narzutu przeglądarki.wp post list --post_type=product --fields=ID --posts_per_page=10odtwarza logikę zapytania w powtarzalnym środowisku, co przydaje się, gdy wolna strona jest za paywallem albo logowaniem. Do profilowania etapów ładowania WordPressa służywp profile stage, ale to osobny pakiet WP-CLI,wp-cli/profile-command, który instalujesz przezwp package install wp-cli/profile-command.
Heurystyka do ustalania priorytetów: każde zapytanie powyżej 50 ms przy rozgrzanym cache jest podejrzane, powyżej 200 ms jest zepsute. web.dev uznaje za dobry TTFB 0,8 sekundy lub mniej. Jeśli jedno WP_Query zjada z tego 250 ms, spora część budżetu znika, zanim szablon zacznie się renderować. TTFB sam nie jest wskaźnikiem Core Web Vitals, ale to pierwsza część czasu LCP, więc każdą jego milisekundę widać w LCP.
Kiedy użyć get_posts zamiast WP_Query
get_posts() to cienka nakładka na WP_Query, która zamiast API pętli zwraca tablicę obiektów WP_Post. W pliku post.php w rdzeniu zawsze ustawia 'no_found_rows' => true i 'ignore_sticky_posts' => true, a domyślnie ma 'suppress_filters' => true.
$posts = get_posts(
[
'post_type' => 'post',
'posts_per_page' => 5,
'fields' => 'ids',
'suppress_filters' => false, // zostaw filtry, gdy wtyczki mają zadziałać
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);Sięgaj po get_posts(), gdy potrzebujesz samej listy, bez pętli z tagami szablonu. Zostań przy WP_Query, gdy potrzebujesz have_posts(), paginacji przez max_num_pages albo jawnej kontroli nad liczeniem wyników.
Uwaga na suppress_filters => true: pre_get_posts i tak się uruchamia, bo WP_Query::get_posts() wywołuje ten hook przez do_action_ref_array() niezależnie od tego argumentu. suppress_filters wyłącza tylko filtry SQL posts_*, na przykład posts_where, posts_join i posts_clauses, oraz the_posts. Wtyczka, która podpina logikę pod te filtry, domyślnie nie działa więc na get_posts(). Jeśli ma działać, ustaw suppress_filters => false.
W callbackach admin AJAX albo REST, które potrzebują tylko ID do pola wyboru, get_posts() z 'fields' => 'ids' jest zwykle czytelniejsze niż pełna pętla. Do renderowania szablonu z the_title() i tagami miniatur zostań przy WP_Query i zresetuj dane po pętli.
Lista kontrolna wydajności WP_Query
Gdy strona działa wolno, przejdź listę po kolei. Pierwsze trzy punkty odpowiadają za większość spowolnień WP_Query w produkcyjnych serwisach WordPress.
- Uruchom Query Monitor na wolnym adresie URL. Posortuj po czasie. Najdłuższe zapytanie prawie zawsze jest tym do poprawienia.
- Sprawdź, czy filtry
meta_queryodwołują się do nieindeksowanegometa_key. Jeśli tak, dodajINDEX (meta_key(32), post_id)nawp_postmeta. - Dodaj
'no_found_rows' => truedo każdej dodatkowej pętli, która nie wyświetla łącznej liczby wyników. - Zastąp
posts_per_page => -1twardym limitem albo pobieraniem partiami przezpaged. - Zastąp
query_posts()przezpre_get_posts(dla głównego zapytania) albonew WP_Query(dla pętli dodatkowych). - Zabezpiecz każdy callback
pre_get_postsprzezis_admin()i$query->is_main_query(). - W pętlach, które wyświetlają tylko tytuł i link, ustaw
'fields' => 'ids'iupdate_post_meta_cache => false. - Po własnych pętlach wywołuj
wp_reset_postdata(), żeby przywrócić globalne$post. - Jeśli to samo WP_Query wykonuje się przy każdym wczytaniu strony, a dane zmieniają się rzadko, zapisz jego ID w transiencie z kluczem opartym na sygnaturze argumentów.
Powiązane: przyspieszanie strony WordPress i optymalizacja bazy danych WordPress.
Poprawki na poziomie zapytań to zwykle najtańszy sposób na odzyskanie budżetu TTFB: tańszy niż wyższy pakiet hostingu, tańszy niż CDN i dużo tańszy niż przebudowa strony na headless.







