| name | wp-plugin-performance |
| description | Performance guidelines for WordPress plugin development: database optimization, object caching, conditional asset loading, efficient hooks, HTTP requests, WP-Cron, AJAX/REST optimization, and common anti-patterns. Based on official WordPress Developer Resources and WP VIP documentation. |
| compatibility | WordPress 6.0+ / PHP 7.4+. Applies to plugins and custom code. |
| license | GPL-2.0-or-later |
| metadata | {"author":"fernando-tellado","version":"1.1"} |
WordPress plugin performance
When to use
Use this skill when:
- Developing new WordPress plugins
- Optimizing existing plugin code for better performance
- Working with database queries (WP_Query, $wpdb, options)
- Implementing caching strategies (object cache, transients)
- Loading assets (scripts, styles) efficiently
- Creating AJAX handlers or REST API endpoints
- Scheduling background tasks with WP-Cron
- Making external HTTP requests from plugins
- Reviewing code before deployment to high-traffic sites
Core performance principles
The performance mantra
Query only what you need
Cache expensive operations
Load assets conditionally
Avoid work on every request
Key concepts
- Bounded queries: Always limit results with
posts_per_page or similar
- Object caching: Store expensive computations for reuse across requests
- Conditional loading: Enqueue scripts/styles only where needed
- Context awareness: Check
is_admin(), page conditions before heavy operations
- Async processing: Move slow tasks to WP-Cron or background processes
Database queries
Efficient database queries are the foundation of plugin performance.
WP_Query optimization
| Parameter | Purpose |
|---|
posts_per_page | Limit results (never use -1 in production) |
no_found_rows | Skip counting total rows when not paginating |
update_post_meta_cache | Set false if not using post meta |
update_post_term_cache | Set false if not using taxonomies |
fields | Request only 'ids' or 'id=>parent' when full objects not needed |
cache_results | Keep true unless intentionally bypassing cache |
WP_Query examples
$query = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 10,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
$post_ids = get_posts( array(
'post_type' => 'product',
'posts_per_page' => 100,
'fields' => 'ids',
'no_found_rows' => true,
) );
$all_posts = get_posts( array(
'post_type' => 'post',
'posts_per_page' => -1, // Never do this in production!
) );
When pagination is needed
$paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
$query = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
) );
echo paginate_links( array(
'total' => $query->max_num_pages,
) );
Avoid query_posts()
query_posts( 'cat=5' );
add_action( 'pre_get_posts', 'ayudawp_modify_main_query' );
function ayudawp_modify_main_query( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
$query->set( 'cat', 5 );
}
}
$custom_query = new WP_Query( array( 'cat' => 5 ) );
Meta queries optimization
Meta queries scan unindexed columns. Use them sparingly.
$query = new WP_Query( array(
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'color',
'value' => 'red',
'compare' => '=',
),
array(
'key' => 'size',
'value' => array( 'S', 'M', 'L' ),
'compare' => 'IN',
),
),
) );
register_taxonomy( 'product_color', 'product', array( ) );
register_taxonomy( 'product_size', 'product', array( ) );
$query = new WP_Query( array(
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'product_color',
'field' => 'slug',
'terms' => 'red',
),
array(
'taxonomy' => 'product_size',
'field' => 'slug',
'terms' => array( 's', 'm', 'l' ),
),
),
) );
Legitimate meta_query uses
Some lookups are unavoidably keyed by post meta because that is where the data lives (Privacy API exporters/erasers keyed by customer email, WooCommerce order number resolvers that respect plugins like Sequential Order Numbers, etc.). For these, the WordPress.DB.SlowDBQuery.slow_db_query_meta_query warning is informational, not a security issue, and the manual reviewer accepts it with a short justification:
$query = new WP_Query( array(
'post_type' => 'ayudawp_withdrawal',
'meta_query' => array(
array(
'key' => '_ayudawp_email',
'value' => $email,
),
),
) );
The justification text matters: state why the meta_query is the right tool here (a contract from another API, a third-party numbering scheme, etc.). A bare phpcs:ignore without comment looks lazy and triggers extra scrutiny.
Post and term exclusion patterns
Plugin Check (and PHPCS WPVIP rules) flag every exclusionary argument: post__not_in, exclude, category__not_in, tag__not_in, author__not_in. The wp.org review pipeline surfaces the warning even when the input set is tiny. Refactor to "fetch a slightly larger window, filter in PHP, cap at the desired count":
$query = new WP_Query( array(
'post__not_in' => $hundreds_of_ids,
'posts_per_page' => 20,
) );
$candidates = get_posts( array(
'posts_per_page' => 60, // 3x desired so the filter rarely empties the list
'fields' => 'ids',
'no_found_rows' => true,
) );
$results = array();
foreach ( $candidates as $id ) {
if ( in_array( $id, $excluded_ids, true ) ) {
continue;
}
$results[] = $id;
if ( count( $results ) >= 20 ) {
break;
}
}
Same pattern applies to get_terms() with exclude:
$terms = get_terms( array(
'taxonomy' => 'product_cat',
'number' => 20,
'exclude' => $excluded_ids,
) );
$candidates = get_terms( array(
'taxonomy' => 'product_cat',
'hide_empty' => false,
'number' => 40,
) );
$results = array();
foreach ( $candidates as $term ) {
if ( in_array( (int) $term->term_id, $excluded_ids, true ) ) {
continue;
}
$results[] = $term;
if ( count( $results ) >= 20 ) {
break;
}
}
The PHP loop is cheap (small bounded window) and the SQL query no longer carries a NOT IN (...) clause that scales linearly with the exclusion list.
Direct database queries
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s
LIMIT %d",
'product',
'publish',
100
)
);
$wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
'%' . $wpdb->esc_like( $search ) . '%' // Leading % = slow
)
);
$wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$wpdb->esc_like( $search ) . '%' // Trailing % = can use index
)
);
Validate before querying
$post_id = get_some_id();
$query = new WP_Query( array( 'p' => intval( $post_id ) ) );
$post_id = get_some_id();
if ( ! empty( $post_id ) && is_numeric( $post_id ) ) {
$query = new WP_Query( array( 'p' => absint( $post_id ) ) );
}
Options and autoload
WordPress loads all autoloaded options on every page request.
Autoload guidelines
| Data type | Autoload | Reason |
|---|
| Plugin settings (small) | Yes | Needed on most requests |
| Feature flags | Yes | Checked frequently |
| Large serialized data | No | Bloats memory on every request |
| Rarely used data | No | Only load when needed |
| Cached API responses | No | Use transients instead |
Managing autoload
add_option( 'ayudawp_settings', array(
'enabled' => true,
'limit' => 10,
) );
add_option( 'ayudawp_large_data', $large_array, '', 'no' );
global $wpdb;
$wpdb->update(
$wpdb->options,
array( 'autoload' => 'no' ),
array( 'option_name' => 'ayudawp_large_data' )
);
$autoload_size = $wpdb->get_var(
"SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE autoload = 'yes'"
);
Avoid frequent option writes
add_action( 'wp_head', 'ayudawp_bad_tracking' );
function ayudawp_bad_tracking() {
$count = get_option( 'page_views', 0 );
update_option( 'page_views', $count + 1 );
}
add_action( 'shutdown', 'ayudawp_buffer_tracking' );
function ayudawp_buffer_tracking() {
wp_cache_incr( 'page_views_buffer', 1, 'ayudawp_stats' );
}
add_action( 'ayudawp_flush_stats', 'ayudawp_flush_view_buffer' );
function ayudawp_flush_view_buffer() {
$buffered = wp_cache_get( 'page_views_buffer', 'ayudawp_stats' );
if ( $buffered ) {
$current = get_option( 'page_views', 0 );
update_option( 'page_views', $current + $buffered );
wp_cache_delete( 'page_views_buffer', 'ayudawp_stats' );
}
}
Object cache
Object cache stores data in memory for the duration of a request (or persistently with Redis/Memcached).
Object cache functions
| Function | Purpose |
|---|
wp_cache_get() | Retrieve cached value |
wp_cache_set() | Store value in cache |
wp_cache_add() | Store only if key doesn't exist |
wp_cache_delete() | Remove cached value |
wp_cache_incr() | Increment numeric value |
wp_cache_get_multiple() | Batch retrieve (WP 5.5+) |
wp_using_ext_object_cache() | Check if persistent cache available |
Caching expensive operations
function ayudawp_get_complex_data( $user_id ) {
$cache_key = 'complex_data_' . $user_id;
$cache_group = 'ayudawp_data';
$data = wp_cache_get( $cache_key, $cache_group );
if ( false === $data ) {
$data = ayudawp_calculate_complex_data( $user_id );
wp_cache_set( $cache_key, $data, $cache_group, HOUR_IN_SECONDS );
}
return $data;
}
add_action( 'profile_update', 'ayudawp_clear_user_cache' );
function ayudawp_clear_user_cache( $user_id ) {
wp_cache_delete( 'complex_data_' . $user_id, 'ayudawp_data' );
}
Expensive functions to cache
These WordPress functions are slow and should be cached:
$post_id = url_to_postid( $url );
$attachment_id = attachment_url_to_postid( $url );
$count = count_user_posts( $user_id );
$oembed = wp_oembed_get( $url );
function ayudawp_cached_url_to_postid( $url ) {
$cache_key = 'url_to_postid_' . md5( $url );
$post_id = wp_cache_get( $cache_key, 'ayudawp_urls' );
if ( false === $post_id ) {
$post_id = url_to_postid( $url );
wp_cache_set( $cache_key, $post_id, 'ayudawp_urls', DAY_IN_SECONDS );
}
return $post_id;
}
function ayudawp_cached_oembed( $url ) {
$cache_key = 'oembed_' . md5( $url );
$html = wp_cache_get( $cache_key, 'ayudawp_embeds' );
if ( false === $html ) {
$html = wp_oembed_get( $url );
if ( $html ) {
wp_cache_set( $cache_key, $html, 'ayudawp_embeds', DAY_IN_SECONDS );
}
}
return $html;
}
Batch cache operations
foreach ( $user_ids as $user_id ) {
$data = wp_cache_get( 'user_data_' . $user_id, 'ayudawp' );
}
$cache_keys = array();
foreach ( $user_ids as $user_id ) {
$cache_keys[] = 'user_data_' . $user_id;
}
$cached_data = wp_cache_get_multiple( $cache_keys, 'ayudawp' );
Transients
Transients provide expiring key-value storage. Without persistent object cache, they use the options table.
Transients vs object cache
| Feature | Transients | Object cache |
|---|
| Expiration | Built-in | Optional |
| Persistence | Database (or object cache) | Memory (or persistent) |
| Use case | Data that expires | Request-level caching |
| Without Redis/Memcached | Uses wp_options | Non-persistent |
Transient best practices
function ayudawp_get_external_data() {
$transient_key = 'ayudawp_api_data';
$data = get_transient( $transient_key );
if ( false === $data ) {
$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( $transient_key, $data, HOUR_IN_SECONDS );
}
}
return $data;
}
foreach ( $users as $user ) {
set_transient( 'user_cache_' . $user->ID, $data, HOUR_IN_SECONDS );
}
foreach ( $users as $user ) {
wp_cache_set( 'user_cache_' . $user->ID, $data, 'ayudawp_users', HOUR_IN_SECONDS );
}
Check object cache availability
function ayudawp_cache_large_data( $key, $data, $expiration ) {
if ( wp_using_ext_object_cache() ) {
set_transient( $key, $data, $expiration );
} else {
ayudawp_file_cache_set( $key, $data, $expiration );
}
}
Conditional asset loading
Load scripts and styles only where they are needed.
Enqueue patterns
add_action( 'wp_enqueue_scripts', 'ayudawp_bad_enqueue' );
function ayudawp_bad_enqueue() {
wp_enqueue_script( 'ayudawp-gallery', AYUDAWP_URL . 'assets/js/gallery.js' );
wp_enqueue_style( 'ayudawp-gallery', AYUDAWP_URL . 'assets/css/gallery.css' );
}
add_action( 'wp_enqueue_scripts', 'ayudawp_conditional_enqueue' );
function ayudawp_conditional_enqueue() {
global $post;
if ( is_singular() && has_shortcode( $post->post_content, 'ayudawp_gallery' ) ) {
wp_enqueue_script( 'ayudawp-gallery', AYUDAWP_URL . 'assets/js/gallery.js', array(), AYUDAWP_VERSION, true );
wp_enqueue_style( 'ayudawp-gallery', AYUDAWP_URL . 'assets/css/gallery.css', array(), AYUDAWP_VERSION );
}
}
add_action( 'wp_enqueue_scripts', 'ayudawp_template_assets' );
function ayudawp_template_assets() {
if ( is_page_template( 'template-contact.php' ) ) {
wp_enqueue_script( 'ayudawp-contact-form', AYUDAWP_URL . 'assets/js/contact.js', array(), AYUDAWP_VERSION, true );
}
}
Block-based conditional loading
function ayudawp_register_block_assets() {
register_block_type( 'ayudawp/custom-block', array(
'editor_script' => 'ayudawp-block-editor',
'editor_style' => 'ayudawp-block-editor-style',
'script' => 'ayudawp-block-frontend', // Only loads when block is used
'style' => 'ayudawp-block-style', // Only loads when block is used
) );
}
add_action( 'init', 'ayudawp_register_block_assets' );
Dequeue unnecessary assets
add_action( 'wp_enqueue_scripts', 'ayudawp_dequeue_unused', 100 );
function ayudawp_dequeue_unused() {
if ( ! has_blocks() ) {
wp_dequeue_style( 'wp-block-library' );
}
if ( function_exists( 'is_woocommerce' ) ) {
if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() && ! is_account_page() ) {
wp_dequeue_style( 'woocommerce-general' );
wp_dequeue_style( 'woocommerce-layout' );
wp_dequeue_script( 'wc-cart-fragments' );
}
}
}
Admin assets
add_action( 'admin_enqueue_scripts', 'ayudawp_admin_assets' );
function ayudawp_admin_assets( $hook ) {
if ( 'settings_page_ayudawp-settings' !== $hook ) {
return;
}
wp_enqueue_style( 'ayudawp-admin', AYUDAWP_URL . 'assets/css/admin.css', array(), AYUDAWP_VERSION );
wp_enqueue_script( 'ayudawp-admin', AYUDAWP_URL . 'assets/js/admin.js', array( 'jquery' ), AYUDAWP_VERSION, true );
}
Efficient hooks
Avoid expensive operations in frequently-called hooks.
Hook execution frequency
| Hook | Frequency | Suitable for |
|---|
plugins_loaded | Every request | Class initialization, early setup |
init | Every request | Register post types, taxonomies |
wp_loaded | Every request | After all plugins loaded |
wp | Frontend requests | Query-dependent setup |
template_redirect | Frontend, before output | Redirects, access control |
admin_init | Admin requests | Admin-only initialization |
wp_head | Frontend, in head | Meta tags, early scripts |
wp_footer | Frontend, in footer | Late scripts |
shutdown | Every request, end | Cleanup, logging |
Context-aware hooks
add_action( 'init', 'ayudawp_expensive_init' );
function ayudawp_expensive_init() {
$data = ayudawp_fetch_remote_config();
}
add_action( 'init', 'ayudawp_smart_init' );
function ayudawp_smart_init() {
if ( wp_doing_ajax() || wp_doing_cron() || defined( 'REST_REQUEST' ) ) {
return;
}
if ( is_admin() ) {
return;
}
ayudawp_frontend_only_setup();
}
add_action( 'template_redirect', 'ayudawp_check_access' );
function ayudawp_check_access() {
if ( is_singular( 'premium_content' ) && ! ayudawp_user_has_access() ) {
wp_redirect( home_url( '/subscribe/' ) );
exit;
}
}
Lazy loading patterns
class AyudaWP_Heavy_Feature {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
}
}
add_action( 'ayudawp_feature_needed', function() {
AyudaWP_Heavy_Feature::get_instance()->run();
} );
Admin notices optimization
add_action( 'admin_notices', 'ayudawp_check_requirements' );
function ayudawp_check_requirements() {
$requirements = ayudawp_expensive_requirements_check();
if ( ! $requirements['met'] ) {
echo '<div class="notice notice-error">...</div>';
}
}
add_action( 'admin_notices', 'ayudawp_cached_requirements_notice' );
function ayudawp_cached_requirements_notice() {
$cache_key = 'ayudawp_requirements_check';
$requirements = get_transient( $cache_key );
if ( false === $requirements ) {
$requirements = ayudawp_expensive_requirements_check();
set_transient( $cache_key, $requirements, HOUR_IN_SECONDS );
}
if ( ! $requirements['met'] ) {
echo '<div class="notice notice-error">...</div>';
}
}
add_action( 'update_option_ayudawp_settings', function() {
delete_transient( 'ayudawp_requirements_check' );
} );
External HTTP requests
HTTP requests to external APIs can significantly slow down page loads.
HTTP request best practices
$response = wp_remote_get( 'https://api.example.com/data' );
$body = wp_remote_retrieve_body( $response );
function ayudawp_fetch_api_data() {
$cache_key = 'ayudawp_api_response';
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$response = wp_remote_get( 'https://api.example.com/data', array(
'timeout' => 5, // 5 seconds max
'sslverify' => true,
) );
if ( is_wp_error( $response ) ) {
error_log( 'AyudaWP API error: ' . $response->get_error_message() );
return ayudawp_get_fallback_data();
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
error_log( 'AyudaWP API returned: ' . $code );
return ayudawp_get_fallback_data();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return ayudawp_get_fallback_data();
}
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
return $data;
}
Move HTTP requests to background
add_action( 'save_post', 'ayudawp_notify_external_service' );
function ayudawp_notify_external_service( $post_id ) {
wp_remote_post( 'https://api.example.com/notify', array(
'body' => array( 'post_id' => $post_id ),
) );
}
add_action( 'save_post', 'ayudawp_schedule_notification' );
function ayudawp_schedule_notification( $post_id ) {
if ( ! wp_next_scheduled( 'ayudawp_send_notification', array( $post_id ) ) ) {
wp_schedule_single_event( time(), 'ayudawp_send_notification', array( $post_id ) );
}
}
add_action( 'ayudawp_send_notification', 'ayudawp_do_notification' );
function ayudawp_do_notification( $post_id ) {
wp_remote_post( 'https://api.example.com/notify', array(
'body' => array( 'post_id' => $post_id ),
'timeout' => 30,
) );
}
WP-Cron
WP-Cron runs on page requests by default. Configure it properly for reliability.
WP-Cron configuration
define( 'DISABLE_WP_CRON', true );
Scheduling events correctly
add_action( 'init', 'ayudawp_schedule_tasks' );
function ayudawp_schedule_tasks() {
wp_schedule_event( time(), 'hourly', 'ayudawp_hourly_task' );
}
add_action( 'init', 'ayudawp_schedule_tasks' );
function ayudawp_schedule_tasks() {
if ( ! wp_next_scheduled( 'ayudawp_hourly_task' ) ) {
wp_schedule_event( time(), 'hourly', 'ayudawp_hourly_task' );
}
}
register_activation_hook( __FILE__, 'ayudawp_activate' );
function ayudawp_activate() {
if ( ! wp_next_scheduled( 'ayudawp_hourly_task' ) ) {
wp_schedule_event( time(), 'hourly', 'ayudawp_hourly_task' );
}
}
register_deactivation_hook( __FILE__, 'ayudawp_deactivate' );
function ayudawp_deactivate() {
wp_clear_scheduled_hook( 'ayudawp_hourly_task' );
}
Batch processing for large datasets
add_action( 'ayudawp_sync_users', 'ayudawp_sync_all_users' );
function ayudawp_sync_all_users() {
$users = get_users();
foreach ( $users as $user ) {
ayudawp_sync_user( $user );
}
}
add_action( 'ayudawp_sync_users_batch', 'ayudawp_sync_users_batch' );
function ayudawp_sync_users_batch() {
$batch_size = 100;
$offset = (int) get_option( 'ayudawp_sync_offset', 0 );
$users = get_users( array(
'number' => $batch_size,
'offset' => $offset,
) );
if ( empty( $users ) ) {
delete_option( 'ayudawp_sync_offset' );
return;
}
foreach ( $users as $user ) {
ayudawp_sync_user( $user );
}
update_option( 'ayudawp_sync_offset', $offset + $batch_size );
wp_schedule_single_event( time() + 30, 'ayudawp_sync_users_batch' );
}
Custom cron intervals
add_filter( 'cron_schedules', 'ayudawp_custom_cron_intervals' );
function ayudawp_custom_cron_intervals( $schedules ) {
$schedules['fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => __( 'Every 15 minutes', 'ayudawp' ),
);
return $schedules;
}
AJAX and REST API optimization
AJAX best practices
jQuery.post( ajaxurl, {
action: 'ayudawp_get_data',
nonce: ayudawp.nonce
}, function( response ) {
});
jQuery.get( ayudawp.rest_url + 'ayudawp/v1/data', {
_wpnonce: ayudawp.nonce
}, function( response ) {
});
Avoid polling patterns
setInterval( function() {
fetch( '/wp-json/ayudawp/v1/updates' );
}, 5000 );
let pollInterval = 5000;
const maxInterval = 60000;
function pollUpdates() {
fetch( '/wp-json/ayudawp/v1/updates' )
.then( response => response.json() )
.then( data => {
if ( data.hasUpdates ) {
pollInterval = 5000;
} else {
pollInterval = Math.min( pollInterval * 1.5, maxInterval );
}
setTimeout( pollUpdates, pollInterval );
} );
}
REST API optimization
register_rest_route( 'ayudawp/v1', '/items', array(
'methods' => 'GET',
'callback' => 'ayudawp_get_items',
'permission_callback' => '__return_true', // Public endpoint
'args' => array(
'per_page' => array(
'default' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => function( $value ) {
return $value > 0 && $value <= 100;
},
),
),
) );
function ayudawp_get_items( $request ) {
$per_page = $request->get_param( 'per_page' );
$cache_key = 'items_' . $per_page;
$items = wp_cache_get( $cache_key, 'ayudawp_api' );
if ( false === $items ) {
$items = get_posts( array(
'post_type' => 'item',
'posts_per_page' => $per_page,
'no_found_rows' => true,
'fields' => 'ids',
) );
wp_cache_set( $cache_key, $items, 'ayudawp_api', 5 * MINUTE_IN_SECONDS );
}
return rest_ensure_response( $items );
}
Common anti-patterns
PHP anti-patterns
if ( in_array( $value, $large_array ) ) {
}
$lookup = array_flip( $large_array );
if ( isset( $lookup[ $value ] ) ) {
}
if ( in_array( $value, $large_array, true ) ) {
}
$html = <<<HTML
<div class="widget">
<h3>{$title}</h3>
<p>{$content}</p>
</div>
HTML;
printf(
'<div class="widget"><h3>%s</h3><p>%s</p></div>',
esc_html( $title ),
wp_kses_post( $content )
);
Cache bypass issues
session_start();
update_user_meta( get_current_user_id(), 'preference', $value );
setcookie( 'visitor_tracking', $id );
N+1 query problems
while ( have_posts() ) {
the_post();
$author_data = get_userdata( get_the_author_meta( 'ID' ) );
$custom_field = get_post_meta( get_the_ID(), 'custom', true );
}
$post_ids = wp_list_pluck( $posts, 'ID' );
$author_ids = wp_list_pluck( $posts, 'post_author' );
update_meta_cache( 'post', $post_ids );
cache_users( $author_ids );
while ( have_posts() ) {
the_post();
$author_data = get_userdata( get_the_author_meta( 'ID' ) );
$custom_field = get_post_meta( get_the_ID(), 'custom', true );
}
Measurement and profiling
Query Monitor
Query Monitor is the essential tool for WordPress performance debugging.
Key panels to check:
- Queries: Identify slow queries, duplicates, queries by component
- Request: Time breakdown by component
- Transients: Transient usage and database hits
- HTTP API Calls: External requests and timing
- Hooks & Actions: Hook execution order and timing
Debug constants
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SAVEQUERIES', true );
define( 'SCRIPT_DEBUG', true );
Custom timing
function ayudawp_measure_operation() {
$start = microtime( true );
ayudawp_expensive_operation();
$elapsed = microtime( true ) - $start;
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( sprintf( 'AyudaWP operation took %.4f seconds', $elapsed ) );
}
}
Server-Timing header
add_action( 'send_headers', 'ayudawp_add_server_timing' );
function ayudawp_add_server_timing() {
global $timestart;
$total = microtime( true ) - $timestart;
header( sprintf( 'Server-Timing: total;dur=%.2f', $total * 1000 ) );
}
Code review checklist
Database queries
Caching
Options
Assets
Hooks
HTTP requests
WP-Cron
AJAX/REST
General
References