WP_Query e The Loop: performance, taxonomias e templates

WP_Query e The Loop: performance, taxonomias e templates

Última verificação: 21 de setembro de 2026
11 min de leitura
Guia
Desenvolvedor full-stack
Core Web Vitals

WP_Query é a classe que o WordPress usa para ir buscar conteúdos à base de dados. Cada arquivo, resultado de pesquisa e página singular começa com uma consulta principal. Loops secundários são instâncias extra de WP_Query que coloca em templates, widgets ou shortcodes. O erro quase sempre é o mesmo: demasiadas consultas, argumentos demasiado largos, ou a variável de paginação errada.

Em alojamento partilhado português (WebHS, PTISP, ou uma VPS própria com nginx + php-fpm) isto aparece primeiro como workers PHP esgotados e TTFB alto, não como um aviso bonito no painel. Este guia cobre consulta principal versus WP_Query secundária, pre_get_posts, flags de desempenho, armadilhas de tax_query, paginação, wp_reset_postdata(), Query Monitor, object cache e quando get_posts() chega.

Referência oficial: WP_Query, pre_get_posts e otimização de performance.

#Consulta principal versus WP_Query secundária

A consulta principal ($wp_the_query / a global $wp_query) é construída antes do template carregar. Define que template o WordPress escolhe (is_home(), is_archive(), is_singular(), etc.) e que posts have_posts() / the_post() mostram no loop padrão.

if ( have_posts() ) :
	while ( have_posts() ) :
		the_post();
		get_template_part( 'template-parts/content', get_post_type() );
	endwhile;
endif;

Um loop secundário é uma nova instância. Não altera a consulta principal e não deve servir para «corrigir» filtros de arquivo.

$related = new WP_Query(
	[
		'post_type'           => 'post',
		'posts_per_page'      => 4,
		'post__not_in'        => [ get_the_ID() ],
		'ignore_sticky_posts' => true,
		'no_found_rows'       => true,
	]
);

if ( $related->have_posts() ) {
	while ( $related->have_posts() ) {
		$related->the_post();
		the_title( '<h3>', '</h3>' );
	}
	wp_reset_postdata();
}

Nunca use query_posts(). Sobrescreve a consulta principal a meio do pedido, parte a paginação e as condicionais, e força um hit extra à base de dados. Se precisa de mudar o que o arquivo mostra, use pre_get_posts. Se precisa de conteúdo extra ao lado da lista principal, use new WP_Query().

Num tema de loja WooCommerce visto em staging em Lisboa, o arquivo de produtos já era a consulta principal. Alguém tinha colocado um segundo WP_Query em archive-product.php «para filtrar a categoria destaque». Resultado: duas listagens, paginação a apontar para o sítio errado, e o Query Monitor a marcar o dobro de SQL em cada pedido.

#Alterar a consulta principal com pre_get_posts

pre_get_posts corre antes do SQL ser montado. Obtém um único hit à base de dados em vez de «ir buscar tudo e filtrar em PHP» ou «correr outro WP_Query por cima».

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 ] ); // ex.: «Newsletter».
		}

		if ( $query->is_post_type_archive( 'caso' ) ) {
			$query->set( 'orderby', 'menu_order title' );
			$query->set( 'order', 'ASC' );
		}
	}
);

Três regras que poupam horas de debug:

  1. Proteja com is_admin() e $query->is_main_query(). Sem isto atinge REST, feeds, listagens de admin e cada loop secundário.
  2. Defina argumentos com $query->set(), não substituindo o objeto inteiro.
  3. Não chame get_posts() nem new WP_Query() dentro do callback sem justificação forte. Cria consultas novas durante a construção da que está a ajustar.

Em arquivos WooCommerce a consulta principal é a lista de produtos. Filtre aí, não com um loop extra em archive-product.php, se o objetivo é o mesmo URL e a mesma paginação.

Feeds (is_feed()) e pesquisa (is_search()) também são consultas principais. Se limitar post_type na home, confirme que não esvazia o RSS nem os resultados de pesquisa. Use condições explícitas por contexto.

#Acelerar WP_Query com no_found_rows e fields ids

Por omissão, WP_Query corre uma consulta que também calcula o total de resultados para paginação (FOUND_ROWS() / historicamente SQL_CALC_FOUND_ROWS). Se não precisa de números de página, desligue:

$ids = new WP_Query(
	[
		'post_type'              => 'post',
		'posts_per_page'         => 20,
		'fields'                 => 'ids',
		'no_found_rows'          => true,
		'update_post_meta_cache' => false,
		'update_post_term_cache' => false,
	]
);

Quando só precisa de IDs (relacionados, listas de exclusão, chaves de transient), fields => 'ids' é a opção mais barata: menos dados na rede, menos hidratação de WP_Post, e pode reutilizar os IDs em post__in.

update_post_meta_cache e update_post_term_cache ajudam quando o loop nunca chama get_post_meta() nem tags de taxonomia. Se o template vai buscar imagem em destaque, categorias ou campos ACF, deixe-os em true (omissão). Desligar e depois ir buscar meta por post gera N+1 pior do que um único warm de cache.

posts_per_page => -1 quase sempre está errado em produção. Defina um número. Se realmente precisa de todos os IDs (migração, CLI), faça-o em WP-CLI ou num job em background, não num pedido HTTP atrás de php-fpm.

#Erros comuns com tax_query no WP_Query

tax_query é poderoso e fácil de tornar caro. Erros comuns:

OR entre taxonomias sem perceber o custo do join. Várias cláusulas relation => 'OR' em taxonomias grandes geram joins largos. Prefira uma taxonomia com vários termos (operator => 'IN') quando fizer sentido.

Slug versus term_id. field => 'slug' lê-se bem, mas term_id mantém-se estável se o slug mudar. Em código que vive anos, guarde o ID do termo em config ou options.

Arrays vazios. Uma lista terms vazia pode dar «zero resultados» ou SQL inesperado conforme a versão e o operador. Valide o input antes de montar a consulta.

meta_query para filtros que deveriam ser taxonomia. Meta é EAV (wp_postmeta). Os índices são mais fracos do que relações de termos. Se o campo é um filtro (setor, cidade, linha de produto), registe uma taxonomia personalizada em vez de pesquisar por meta_key.

$args = [
	'post_type'      => 'post',
	'posts_per_page' => 10,
	'no_found_rows'  => true,
	'tax_query'      => [
		'relation' => 'AND',
		[
			'taxonomy' => 'category',
			'field'    => 'term_id',
			'terms'    => [ 4, 9 ],
			'operator' => 'IN',
		],
		[
			'taxonomy' => 'post_tag',
			'field'    => 'slug',
			'terms'    => [ 'wordpress', 'performance' ],
			'operator' => 'AND',
		],
	],
];

include_children é true por omissão em taxonomias hierárquicas. Em listas planas onde quer exatamente um termo, defina include_children => false de forma explícita.

Num projeto B2B com filtro «distrito» em postmeta, a listagem abria em 1,8 s de TTFB. Depois de migrar o filtro para uma taxonomia distrito e tax_query com term_id, o mesmo arquivo ficou sob 400 ms de SQL na mesma máquina MariaDB. O custo não era PHP; era o JOIN em wp_postmeta sem índice útil para aquele meta_key.

#Paginação: paged versus page

O WordPress usa duas query vars:

  • paged para arquivos, índice do blog e a maioria das listas personalizadas.
  • page para paginação dentro de um conteúdo de página estática / front page (<!--nextpage-->).

Num loop secundário num arquivo:

$paged = max( 1, (int) get_query_var( 'paged' ) );

$list = new WP_Query(
	[
		'post_type'      => 'post',
		'posts_per_page' => 10,
		'paged'          => $paged,
	]
);

Numa página definida como front page estática (is_front_page() && is_page()) a consulta principal lê muitas vezes page, não paged. Muitos temas falham aqui: as ligações parecem corretas, mas o segundo loop começa sempre na página 1. Leia ambas quando tiver dúvidas:

$paged = (int) get_query_var( 'paged' );
if ( $paged < 1 ) {
	$paged = (int) get_query_var( 'page' );
}
$paged = max( 1, $paged );

Para ligações de paginação em loops secundários: passe total e current a paginate_links() com base em $list->max_num_pages e $paged, não na global $wp_query. Caso contrário «página seguinte» aponta para o número de página do arquivo principal.

Quando o loop não deve paginar, defina no_found_rows => true e salte paginate_links(). Poupa a consulta de contagem por completo.

#wp_reset_postdata e a global $post

$related->the_post() define a global $post. Depois do loop tem de chamar wp_reset_postdata() para que o resto do template (sidebar, rodapé, widgets relacionados) continue a ver o post principal.

$q = new WP_Query( $args );
while ( $q->have_posts() ) {
	$q->the_post();
	// template
}
wp_reset_postdata();

wp_reset_query() é da era do query_posts(). Não o use em código novo. Se iterar um array de WP_Post sem the_post(), use setup_postdata( $post ) dentro do loop e wp_reset_postdata() depois, ou evite template tags globais e leia $post->post_title diretamente.

Se esquecer o reset, get_the_ID() falha no rodapé, em blocos de schema e em módulos «leia também» que esperam o post principal. Num tema filho usado por uma equipa de conteúdo no Porto, o JSON-LD do artigo apontava para o último post do bloco «relacionados» porque o reset tinha sido omitido após um refactor.

#Encontrar consultas lentas com Query Monitor

Query Monitor mostra SQL, call stack e que componente é dono da consulta. Fluxo de trabalho que funciona em staging:

  1. Abra a página que parece lenta (arquivo, lista de produtos, home com três blocos «destaque»).
  2. Veja o separador Queries: ordene por tempo. Encontre linhas WP_Query com duração alta ou muitas consultas iguais.
  3. Veja Duplicate Queries: muitas vezes é o mesmo tax_query no header, no hero e na lista principal.
  4. Veja Hooks: confirme que o seu callback de pre_get_posts corre mesmo na consulta principal.

Em alojamento partilhado em Portugal costuma haver memory_limit apertado e poucos workers php-fpm. Dez WP_Query extra na home multiplicam-se sob tráfego de campanha. Query Monitor em local ou staging dá números; em produção use New Relic / Blackfire se tiver, ou QM temporário atrás de restrição por IP.

#Object cache e WP_Query

WP_Query não guarda automaticamente o conjunto completo de resultados entre pedidos. Aquece caches de post, meta e termo para os IDs que obtém. Com Redis ou Memcached como object cache (comum em WordPress gerido e VPS própria) lookups repetidos de get_post() / meta ficam mais baratos, mas o primeiro SQL de uma combinação única de argumentos continua a ir ao MySQL.

Padrões práticos:

  • Transient à volta de listas de IDs. Faça cache do array de IDs de um tax_query caro, TTL 5-15 minutos, invalide em save_post para esse post type.
  • Não faça cache de HTML completo no mesmo transient que a lista de IDs se o conteúdo for personalizado ou tiver partes dependentes de nonce.
  • Sticky posts e ordem post__in. Quando fizer cache de IDs, lembre-se de orderby => 'post__in' na reidratação.

Sem object cache, os transients ficam em wp_options (autoload pode tornar-se um problema). Em alojamento económico sem Redis: TTLs curtos, poucas chaves, e limpe transients mortos. Com Redis: use um prefixo de chave por ambiente (prod:, staging:) para o cache não vazar entre slots de deploy.

Full page cache (nginx FastCGI cache, Cloudflare, ou HTML cache por plugin) fica à frente do php-fpm. Um WP_Query barato continua a ajudar em cache miss, preview, utilizadores autenticados e páginas de carrinho WooCommerce onde o HTML não é cacheado. Otimize a consulta na mesma; não confie só no edge cache.

#Quando usar get_posts em vez de WP_Query

get_posts() é um wrapper fino sobre WP_Query com alguns valores por omissão (suppress_filters historicamente, comportamento semelhante a no_found_rows em chamadas simples). Use-o quando:

  • precisa de um array de posts ou IDs,
  • não precisa do loop have_posts(),
  • não pagina.
$ids = get_posts(
	[
		'post_type'              => 'post',
		'posts_per_page'         => 5,
		'fields'                 => 'ids',
		'no_found_rows'          => true,
		'update_post_meta_cache' => false,
		'update_post_term_cache' => false,
	]
);

Escolha new WP_Query() quando precisa de found_posts, max_num_pages, paginação própria, ou controlo explícito sobre filtros. Escolha pre_get_posts quando altera o arquivo em que o utilizador já está. Escolha get_posts() para listas auxiliares pequenas e sem paginação em PHP.

A REST API (/wp/v2/posts) e o Query Loop do editor de blocos são caminhos próprios. Também acabam em WP_Query, mas os seus filtros em pre_get_posts têm de respeitar is_admin(), contexto REST e WP_Query::is_main_query() para não alterar respostas da API por acidente.

WP_Query é síncrono no pedido. Evite loops secundários pesados em wp_footer «porque é prático». Coloque-os no sítio do template onde o HTML é mesmo necessário, ou carregue via REST/Ajax se o bloco está abaixo da dobra.

#Checklist de WP_Query antes do merge

  1. É a consulta principal? Use pre_get_posts, não um loop extra.
  2. Precisa de paginação? Se não: no_found_rows => true. Se sim: paged / page corretos.
  3. Só precisa de IDs? fields => 'ids'.
  4. O filtro é taxonomia, não meta?
  5. Chame wp_reset_postdata() após cada loop secundário que use the_post().
  6. Confirme com Query Monitor que o número de SQL não duplica na home.
  7. Com object cache: faça cache de listas de IDs, não da assunção «o WordPress faz cache da consulta por mim».

Leitura interna: como acelerar um site baseado em WordPress. Externa: a referência oficial de WP_Query, pre_get_posts e advanced administration: performance.

Próximo passo

Transforme o artigo numa implementação real

Este bloco reforça a ligação interna e conduz o leitor para o passo seguinte mais útil dentro da arquitetura do site.

Quer implementar isto no seu site?

Se o problema está nos Core Web Vitals, no rendering lento ou no peso do WordPress, posso mapear e implementar a otimização.

Cluster relacionado

Explorar outros serviços WordPress e base de conhecimento

Reforce o seu negócio com suporte técnico profissional em áreas-chave do ecossistema WordPress.

FAQ do artigo

Perguntas frequentes

Respostas práticas para aplicar o tema na execução real.

SEO-readyGEO-readyAEO-ready1 Q&A
O que é O guia definitivo para wp_Query e the loop (edição 2026)?#
É um guia prático de WP_Query e The Loop em PHP 8.4: loop principal vs secundário, porque `query_posts()` é proibido, e como argumentos errados geram N+1 e matam TTFB em listagens.

Precisa de FAQ adaptado ao setor e mercado? Criamos uma versão alinhada com os seus objetivos de negócio.

Fale connosco

Artigos Relacionados