`WP_Query` es el constructor SQL detrás de cada petición del front-end: archivos, entradas individuales, endpoints REST y bucles de tienda de WooCommerce. Cuando una página va lenta, la causa suele ser una de tres cosas: un `meta_query` contra una `meta_key` sin índice, un `posts_per_page` sin límite, o `SQL_CALC_FOUND_ROWS` ejecutándose en cada carga cuando la página no muestra ningún total.
Orden de magnitud en una tienda WooCommerce real con 50.000 productos: un meta_query contra _stock_status sin índice sobre (meta_key, post_id) tarda entre 1,5 y 3 segundos. Tras añadir el índice compuesto, la misma consulta baja a 50-200 ms. La consulta es idéntica; lo único que cambió fue el índice.
El resto de esta guía es la memoria muscular necesaria para evitar esos tres modos de fallo y recuperar el control cuando un tema heredado ya los trae cableados. El diseño de consultas de taxonomías y meta, los índices compuestos y la caché de consultas añadida en WordPress 6.1 tienen su propio artículo: WP_Query avanzado: taxonomías, meta y rendimiento. Referencias oficiales: WP_Query, pre_get_posts y el código fuente de class-wp-query.php.
Cómo construye y ejecuta WP_Query una petición
Para dominar WP_Query hay que entender qué ocurre cuando lo instancias. WordPress ejecuta estos pasos:
- Análisis de argumentos: convierte tu array de argumentos a un formato estandarizado.
- Generación del SQL: construye una sentencia SQL SELECT compleja.
- Ejecución de la consulta: envía la petición a la base de datos.
- Llenado del objeto: puebla el objeto
WP_Querycon objetos de entrada y metadatos.
Propiedades del objeto WP_Query
Cuando ejecutas $query = new WP_Query($args), no obtienes solo un array de entradas. Obtienes un objeto enorme con propiedades clave:
$query->posts: un array de objetosWP_Post.$query->post_count: número de entradas que se muestran en la página actual.$query->found_posts: número total de entradas que cumplen los criterios (sin tener en cuenta la paginación).$query->max_num_pages: número total de páginas de resultados.$query->query_vars: los argumentos que WordPress usó realmente para ejecutar la consulta.
Consulta principal frente a WP_Query secundario
La consulta principal ($wp_the_query, expuesta como el $wp_query global) se construye antes de cargar la plantilla. Decide qué plantilla elige WordPress (is_home(), is_archive(), is_singular() y el resto) y qué entradas muestra el loop por defecto con have_posts() y the_post(). Un loop secundario es una instancia de WP_Query aparte: no modifica la consulta principal y no sirve para corregir a posteriori los filtros de un archivo. Para cambiar lo que muestra un archivo, usa pre_get_posts; para mostrar contenido adicional junto a la lista principal, usa new WP_Query().
1. El loop principal (contexto global)
Es el loop que dispara la petición de la URL. WordPress se encarga de la instanciación; tú solo iteras:
if ( have_posts() ) :
while ( have_posts() ) : the_post();
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
endif;2. El loop secundario (consultas personalizadas)
Se usa para entradas relacionadas, widgets de últimas noticias o cualquier sección que saque una lista fuera de la consulta principal. En el código real se usan tres constructores, y no son intercambiables:
new WP_Query( $args ), objeto completo: exponefound_postsymax_num_pagesy admite iteración conthe_post(). Úsalo cuando necesites paginación o acceso completo a las template tags.get_posts( $args ), envoltorio ligero que devuelve un array de objetosWP_Post. Por defecto usa'suppress_filters' => truey siempre fija'no_found_rows' => truee'ignore_sticky_posts' => true, lo que es más rápido pero se salta los filtrosposts_*. Úsalo para listas cortas de tamaño fijo en barras laterales.query_posts(), destructivo: sustituye el$wp_queryglobal por una consulta nueva, rompe los condicionalesis_*()más abajo en la página y ejecuta una segunda consulta principal encima de la que WordPress ya ejecutó. La referencia de la función dice que no está pensada para usarse en plugins ni temas. No la uses. Si la encuentras en un tema heredado, sustitúyela porpre_get_postspara la consulta principal y pornew WP_Querypara todo lo demás.
$args = [
'post_type' => 'post',
'posts_per_page' => 5,
'no_found_rows' => true, // CRÍTICO PARA EL RENDIMIENTO
];
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
// Renderizar título, extracto, etc.
}
wp_reset_postdata(); // OBLIGATORIO para restaurar el objeto global $post
}Cómo cambiar la consulta principal con pre_get_posts
La referencia de pre_get_posts indica que el hook se dispara después de crear el objeto de variables de consulta y antes de ejecutar la consulta. Ese momento es justo la clave: cambias los argumentos una vez y WordPress sigue ejecutando una sola consulta, en lugar de ejecutar la consulta principal y después un segundo WP_Query encima.
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' );
}
}
);Tres reglas que salen de la página de referencia y del core:
- Llama a los condicionales como métodos del objeto recibido (
$query->is_home()), no como funciones globales. La función globalis_main_query()lee$wp_query, no la consulta que estás filtrando. En el core,WP_Query::is_main_query()es una sola comparación:$wp_the_query === $this. - Protege el callback con
is_admin()y$query->is_main_query(). Sin ambas comprobaciones, el callback también cambia las tablas de listado del admin, los feeds, las peticiones REST y cada loop secundario de la página. is_front_page()no funciona dentro del hook, porque la consulta aún no está preparada;is_home()sí. Para una portada estática, revisa las query vars en su lugar.
Usa $query->set() para cambiar argumentos y nunca llames a query_posts() ni a un segundo new WP_Query() desde dentro del callback.
Qué hacen the_post() y have_posts() detrás del loop
Leer class-wp-query.php explica varios errores que en las plantillas parecen aleatorios.
- En la primera llamada a
the_post(), el core precarga las cachés de autores de todo el conjunto de resultados y dispara la acciónloop_start. Si la consulta no devolvió objetos de entrada completos, en ese momento también precarga las cachés de entradas, meta y términos (salvo queupdate_post_meta_cacheoupdate_post_term_cacheseanfalse). - Con
'fields' => 'ids', el core actual sigue convirtiendo cada ID en un objeto de entrada completo conget_post()dentro dethe_post(), así que el loop funciona, pero entonces la lista de IDs cuesta lo mismo que una consulta completa. setup_postdata()fija más globales que$post:$id,$authordata,$currentday,$currentmonth,$page,$pages,$multipage,$morey$numpages, y después dispara la acciónthe_post. Un loop secundario sin reset deja todas ellas apuntando a la última entrada relacionada, y por eso una caja de autor o una entrada paginada (<!--nextpage-->) se rompe después de un bloque de entradas relacionadas.- Cuando
have_posts()se queda sin entradas, disparaloop_endy rebobina la consulta. Si la consulta no encontró nada, disparaloop_no_resultsen su lugar.
Cómo usar wp_reset_postdata después de un loop secundario
Llamar a the_post() sobre una consulta secundaria cambia el $post global. A partir de ahí, template tags como the_title() o get_permalink() se comportan como si siguieran en la última entrada de ese loop, hasta que llamas a wp_reset_postdata() al terminarlo.
wp_reset_postdata o wp_reset_query
En wp-includes/query.php la diferencia son dos líneas. wp_reset_postdata() llama a $wp_query->reset_postdata(), que restaura el $post global y las globales anteriores a partir de la consulta principal. wp_reset_query() primero vuelve a poner $wp_the_query en $wp_query y luego llama a wp_reset_postdata(). Solo necesitas wp_reset_query() después de query_posts(), y ahí la solución es eliminar query_posts().
Cuando recorres con foreach un array devuelto por get_posts(), las template tags no saben nada de la entrada hasta que llamas a setup_postdata( $post ); al terminar, haz el reset de la misma forma:
global $post;
$items = get_posts( [ 'numberposts' => 3 ] );
foreach ( $items as $post ) {
setup_postdata( $post );
the_title( '<li>', '</li>' );
}
wp_reset_postdata();setup_postdata() no asigna por sí misma el $post global, y por eso aquí la variable del loop es el $post global.
Errores de rendimiento comunes en WP_Query
1. Por qué posts_per_page => -1 agota la memoria
-1 indica a WP_Query que cargue en memoria cada fila coincidente como objeto WP_Post hidratado y que después precargue las cachés de meta y de términos de todas ellas (update_postmeta_cache() y update_object_term_cache() en el core). En un CPT que pasó de 50 entradas a 5.000 en dos años, el mismo código que tardaba 80 ms ahora reserva más de 200 MB de memoria PHP y agota el techo de memory_limit en un hosting compartido.
La solución es un límite duro que coincida con lo que se renderiza:
- Un widget lateral que muestra 5 elementos lleva
'posts_per_page' => 5. - Un generador de sitemap que de verdad necesita todas las filas debe procesar por lotes con
pageddentro de un bucle, no con-1. 200 IDs por lote es un techo seguro en servidores habituales. - Las herramientas internas de exportación, donde la presión de memoria es aceptable, deberían usar aun así
'fields' => 'ids'para no hidratar objetos de entrada completos.
2. Cuándo evitar SQL_CALC_FOUND_ROWS con no_found_rows
Sin no_found_rows, y siempre que la consulta tenga LIMIT, WP_Query añade SQL_CALC_FOUND_ROWS al SELECT y luego ejecuta SELECT FOUND_ROWS() (a través del filtro found_posts_query). MySQL tiene que seguir contando después de alcanzar el LIMIT, lo que en una tabla wp_posts de 200.000 filas con un JOIN de tax_query suele añadir 80-250 ms por petición, según el estado de la caché.
Si la página no muestra “Página 3 de 47” ni un total, fija 'no_found_rows' => true. La pregunta siguiente siempre es “pero quiero paginación con enlaces de siguiente y anterior”, y eso funciona sin found_posts siempre que compruebes have_posts() contra una consulta que pide de más (por ejemplo, pide posts_per_page + 1 y renderiza posts_per_page; la fila extra indica que existe una página siguiente).
Para archivos paginados que de verdad muestran un total (“Mostrando 1-10 de 4.200 artículos”), conserva el conteo, pero guárdalo en un transient de 5 minutos con una clave basada en la firma de los filtros. El total rara vez cambia en cinco minutos; el escaneo del JOIN no tiene por qué repetirse en cada carga.
3. Ordenar de forma aleatoria (orderby => rand)
ORDER BY RAND() obliga a MySQL a asignar un valor aleatorio a cada fila coincidente y a ordenar el conjunto completo, normalmente en una tabla temporal, antes de aplicar el LIMIT. Además, el core se niega a cachearlo: la caché de consultas de WordPress 6.1 se salta cualquier petición cuyo ORDER BY contenga RAND(.
Solución técnica:
- Obtén los IDs de las últimas 50 entradas.
- Elige 5 IDs aleatorios en PHP.
- Ejecuta una segunda consulta con
post__in => $random_ids.
4. meta_query contra una meta_key sin indexar
Es la causa más frecuente de páginas de archivo lentas en instalaciones antiguas de WooCommerce. El schema.php del core crea wp_postmeta con dos índices de una sola columna, post_id y un prefijo de meta_key, y ningún índice compuesto. El índice de meta_key tiene una cardinalidad demasiado baja para ayudar cuando WordPress filtra por meta_key = '_stock_status' AND meta_value = 'instock': MySQL sigue teniendo que recorrer todas las filas con esa clave.
Un caso real: una tienda WooCommerce polaca con 50.000 productos. La página de archivo de la tienda enganchaba pre_get_posts para añadir un meta_query que filtraba por _stock_status y por un campo personalizado _brand. El TTFB P95 de la página de tienda era de 4,1 segundos. SAVEQUERIES mostraba el 80% del tiempo dentro de un único JOIN contra wp_postmeta. La corrección:
ALTER TABLE wp_postmeta
ADD INDEX idx_meta_key_post_id (meta_key(32), post_id);Tras el índice, la misma consulta bajó a 180 ms. No cambió ni una línea de PHP. Para valores que participan en comparaciones de rango reales ('compare' => '>', 'compare' => 'BETWEEN'), también necesitas un índice sobre meta_value, normalmente de prefijo, porque la columna es LONGTEXT.
Cuando el conjunto de valores es fijo (tallas, colores, slugs de marca), no uses meta_query en absoluto. Registra una taxonomía personalizada. wp_term_relationships tiene una clave primaria sobre (object_id, term_taxonomy_id) y un índice aparte sobre term_taxonomy_id, así que el mismo filtro que tardaba 1,8 s como meta_query se ejecuta en 30-60 ms como tax_query.
5. Las cláusulas de tax_query se acumulan a escala
WP_Tax_Query primero resuelve tus términos en valores term_taxonomy_id y después cada cláusula IN añade un LEFT JOIN contra wp_term_relationships. Las cláusulas AND se convierten en una subconsulta COUNT correlacionada, NOT IN en una subconsulta, y solo EXISTS y NOT EXISTS tocan wp_term_taxonomy. Cada taxonomía extra en la misma consulta añade otro join o subconsulta que MySQL tiene que combinar, así que las páginas de filtros facetados que combinan seis taxonomías (talla, color, marca, temporada, material, rango de precio) son las primeras en sufrirlo. Las salidas son desnormalizar en una única taxonomía de facetas, pasar a FacetWP o SearchWP, que indexan fuera de las tablas, o llevar las facetas a un motor de búsqueda (Elasticsearch mediante ElasticPress, o Algolia para catálogos de solo lectura).
Ejemplos de tax_query y date_query en WP_Query
1. Relaciones con tax_query
Consultar varias categorías o etiquetas requiere el argumento tax_query. Usa el parámetro relation para elegir entre lógica AND y 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. Consultas por fecha (rango dinámico)
Las consultas de fecha nativas son potentes y eficientes. Úsalas en lugar de filtrar a mano con SQL.
$args = [
'date_query' => [
[
'after' => 'January 1st, 2025',
'before' => [
'year' => 2026,
'month' => 12,
'day' => 31,
],
'inclusive' => true,
],
],
];Arreglar el 404 de la paginación de WP_Query en loops personalizados
Uno de los problemas más habituales es “la paginación de mi página personalizada devuelve un 404”.
Por qué ocurre: WordPress no sabe en qué número de página estás en un WP_Query personalizado. Las URL de archivo (/page/2/) fijan la query var paged; una portada estática fija page en su lugar.
La solución:
$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,
// ...
];No combines offset con paged. El argumento offset anula el cálculo de la página, así que todas las páginas devuelven el mismo tramo. Si necesitas “una entrada destacada y después una lista paginada”, ejecuta dos consultas o calcula tú el offset de cada página.
Cómo cachear resultados de WP_Query
En 2026, un sitio con mucho tráfico rara vez debería tocar la base de datos para listas estáticas. Desde WordPress 6.1, el core ya cachea en el object cache la lista de IDs de consultas idénticas repetidas (cache_results); los detalles están en la guía avanzada enlazada al principio. Los patrones de abajo cubren lo que esa caché no cubre.
1. Usar la API de Transients
Si tienes una consulta compleja (por ejemplo, una lista localizada de “Ofertas de empleo en tendencia”), guarda los IDs en un transient durante 1 hora. No guardes el objeto WP_Query completo: serializa cada entrada y el estado de la consulta, y la copia cacheada ya no se comporta como una consulta nueva.
$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,
] );Borra el transient en save_post para el tipo de contenido que lista. Sin un object cache persistente, los transients se guardan en wp_options.
2. Desactivar la precarga de caché en una consulta concreta
Después de obtener los IDs de las entradas, WP_Query llama a update_postmeta_cache() y update_object_term_cache() para cargar en bloque en el object cache cada fila de meta y cada fila de términos de esas entradas. Es el valor por defecto correcto para un loop de plantilla normal que va a llamar a get_post_meta() y get_the_terms(). Es un desperdicio en un loop que solo renderiza el título y el enlace permanente.
$args = [
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'no_found_rows' => true,
'fields' => 'ids',
];En un archivo de 50 entradas con 30 filas de meta cada una, saltarse la precarga de meta evita leer y cachear 1.500 filas en una sola petición. Combinado con 'fields' => 'ids', te ahorras por completo la hidratación de objetos WP_Post, algo útil en generadores de sitemap, listas de IDs de entradas relacionadas en la barra lateral y widgets del escritorio de administración, siempre que leas $q->posts directamente y no lo recorras con the_post().
Cuándo divide WP_Query una consulta en IDs y objetos
El core no siempre obtiene las filas completas en un solo SELECT. En WP_Query::get_posts(), cuando el SQL no ha sido modificado por filtros posts_* y hay un object cache externo activo o la consulta tiene LIMIT con posts_per_page por debajo de 500, WordPress selecciona primero solo los ID y después rellena los objetos de entrada, desde el object cache cuando ya están ahí. El filtro split_the_query permite activar o desactivar este comportamiento.
Dos consecuencias para la depuración:
- El SQL que ves en Query Monitor puede ser
SELECT wp_posts.ID ...seguido de una consulta aparte para las entradas que faltan. Es comportamiento del core, no un fallo de un plugin. - Cualquier plugin que cambie el SQL mediante los filtros de cláusulas
posts_*desactiva la división. Un plugin que cambie los campos seleccionados (posts_fields,posts_fields_request,posts_clauses_request) también desactiva la caché de consultas de 6.1 para esa petición.
También existe un atajo completo: el filtro posts_pre_query se ejecuta antes de la llamada a la base de datos y, si devuelve un array, WordPress se salta el SQL por completo. Los plugins de búsqueda lo usan para responder a WP_Query desde un índice externo. Si Query Monitor muestra un WP_Query con resultados y sin SQL correspondiente, busca un callback en posts_pre_query.
Modificar resultados de WP_Query en la REST API
Si construyes un frontend en React con WordPress headless, no usas WP_Query directamente en JS, pero la REST API lo usa en el backend.
Filtrar los argumentos de consulta de la REST API
Puedes usar el filtro rest_{post_type}_query para cambiar cómo consulta la API la base de datos según los parámetros de la URL. Revisa antes el endpoint: /wp/v2/posts ya acepta un parámetro nativo exclude, así que uno propio solo tiene sentido para lógica que el core no cubre.
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() convierte una cadena separada por comas en un array de enteros positivos, así que un parámetro manipulado no puede inyectar nada más en la consulta.
Cómo depurar consultas lentas de WP_Query
Las cuatro herramientas que de verdad encuentran la respuesta:
- Query Monitor (John Blackbourn). Instálalo en staging, nunca en producción. El panel de consultas agrupa por componente, así que ves enseguida si el TTFB de 2 segundos venía de tu tema, del core de WooCommerce o de un plugin de terceros enganchado a
pre_get_posts. La pestaña de consultas duplicadas detecta los bucles N+1, en los que una template tag dentro dethe_post()vuelve a consultar la base de datos por cada fila. SAVEQUERIES. Pondefine( 'SAVEQUERIES', true );enwp-config.phpsolo en staging, porque cada consulta de la petición se queda en memoria. WordPress rellena entonces$wpdb->queries; cada entrada guarda el SQL, su duración, la pila de llamadas, la hora de inicio y datos personalizados opcionales (wpdb::log_query()en el core).error_log( print_r( $wpdb->queries, true ) )enshutdownproduce la traza de referencia de “qué se ejecutó y cuánto tardó”.$query->requestconEXPLAIN. Después denew WP_Query(),echo $query->request;imprime el SQL que WordPress generó de verdad. Pasa esa cadena porEXPLAINen TablePlus o conmysql -e. Buscatype: ALL(escaneo completo de la tabla),Using filesortoUsing temporary: esas son las filas que necesitan un índice.- WP-CLI.
wp db query --skip-column-names "EXPLAIN $SQL"ejecutaEXPLAINcontra la base de datos en vivo sin la sobrecarga del navegador.wp post list --post_type=product --fields=ID --posts_per_page=10reproduce la lógica de la consulta desde un entorno determinista, útil cuando la página lenta está detrás de un muro de pago o de un inicio de sesión.
Una heurística para priorizar: cualquier consulta por encima de 50 ms con la caché caliente es sospechosa, y cualquier cosa por encima de 200 ms está rota. web.dev sitúa un buen TTFB en 0,8 segundos o menos; si un solo WP_Query se come 250 ms, una buena parte del presupuesto se ha ido antes de que la plantilla empiece a renderizar. El TTFB no es en sí mismo una métrica de Core Web Vitals, pero es la primera parte del tiempo de LCP, así que cada milisegundo se nota en el LCP.
Cuándo usar get_posts en lugar de WP_Query
get_posts() es un envoltorio ligero sobre WP_Query que devuelve un array de objetos WP_Post en lugar de la API del loop. En el post.php del core siempre fija 'no_found_rows' => true e 'ignore_sticky_posts' => true, y por defecto usa 'suppress_filters' => true.
$posts = get_posts(
[
'post_type' => 'post',
'posts_per_page' => 5,
'fields' => 'ids',
'suppress_filters' => false, // deja los filtros activos si los plugins deben actuar
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);Usa get_posts() cuando necesites una lista y no un loop con template tags. Quédate con WP_Query cuando necesites have_posts(), paginación mediante max_num_pages o control explícito sobre el recuento de resultados.
Ojo con suppress_filters => true: pre_get_posts se dispara igualmente, porque WP_Query::get_posts() lanza ese hook con do_action_ref_array() sin mirar ese argumento. suppress_filters solo desactiva los filtros SQL posts_*, como posts_where, posts_join y posts_clauses, además de the_posts. Un plugin que engancha su lógica en esos filtros no actúa sobre get_posts() por defecto; fija suppress_filters => false cuando deba hacerlo.
Para callbacks de AJAX del admin o de la REST API que solo necesitan IDs para un campo de selección, get_posts() con 'fields' => 'ids' suele ser más claro que un loop completo. Para renderizar plantillas con the_title() y etiquetas de miniatura, quédate con WP_Query y haz el reset al terminar.
Checklist de rendimiento de WP_Query
Cuando una página va lenta, recorre la lista en orden: los tres primeros puntos explican la mayor parte de la lentitud de WP_Query en sitios WordPress en producción.
- Lanza Query Monitor sobre la URL lenta. Ordena por tiempo. La consulta más larga es casi siempre la que hay que arreglar.
- Comprueba si los filtros
meta_queryapuntan a unameta_keysin indexar. Si es así, añadeINDEX (meta_key(32), post_id)sobrewp_postmeta. - Añade
'no_found_rows' => truea cada loop secundario que no muestre un total. - Sustituye
posts_per_page => -1por un límite duro o procesa por lotes conpaged. - Sustituye
query_posts()porpre_get_posts(para la consulta principal) onew WP_Query(para los loops secundarios). - Protege cada callback de
pre_get_postsconis_admin()y$query->is_main_query(). - En loops que solo renderizan título y enlace, usa
'fields' => 'ids'yupdate_post_meta_cache => false. - Después de los loops personalizados, llama a
wp_reset_postdata()para restaurar el$postglobal. - Si el mismo WP_Query se ejecuta en cada carga y los datos cambian poco, cachea sus IDs en un transient con una clave basada en la firma de los argumentos.
Lectura relacionada: optimización de velocidad de WordPress.
Las correcciones a nivel de consulta suelen ser la forma más barata de recuperar el presupuesto de TTFB: más barata que mejorar el plan de hosting, más barata que añadir una CDN y mucho más barata que rehacer el sitio en headless.
¿Necesitas ayuda con desarrollo WordPress o mantenimiento WordPress? Contáctanos.






