# WordPress Performance Anti-Patterns Complete catalog of performance anti-patterns for WordPress code review. ## Quick Lookup Table | Pattern to Find | Severity | Issue | |-----------------|----------|-------| | `posts_per_page => -1` | CRITICAL | Unbounded query | | `numberposts => -1` | CRITICAL | Unbounded query | | `query_posts()` | CRITICAL | Replaces main query, breaks pagination | | `session_start()` | CRITICAL | Cache bypass | | `setInterval.*fetch` | CRITICAL | Polling/self-DDoS | | `intval($var)` in query args | CRITICAL | Falsy → 0 → no WHERE | | `update_option` on frontend | CRITICAL | DB write per request | | `cache_results => false` | WARNING | Disables query cache | | `LIKE '%...%'` | WARNING | Full table scan | | `post__not_in` | WARNING | Slow exclusion, filter in PHP instead | | `meta_query` with `value` | WARNING | Unindexed scan | | `wp_remote_get` uncached | WARNING | Blocking HTTP | | `$.post(` for reads | WARNING | Bypasses cache | | `add_option` without autoload=no | WARNING | Bloats alloptions | | `setcookie()` on public pages | WARNING | Prevents caching | | `url_to_postid()` | WARNING | Uncached lookup | | `get_template_part` in loops | WARNING | Repeated file I/O | | `admin-ajax.php` | WARNING | Full WP bootstrap | | `in_array()` without strict | WARNING | O(n) complexity at scale | | `import _ from 'lodash'` | WARNING | Full library import bloats bundle | | Heredoc/nowdoc syntax | WARNING | Prevents late escaping | | Page builder plugins | WARNING | High query count | | Infinite scroll with POST | WARNING | Uncached requests | | Many `registerBlockStyle()` | WARNING | Creates iframe per style | | Missing script version | INFO | Cache busting issues | | Missing `no_found_rows` | INFO | Unnecessary count | ## Database Query Anti-Patterns ### Unbounded Queries (CRITICAL) Queries without limits can return millions of rows, causing OOM errors and timeouts. ```php // ❌ BAD: No limit - returns ALL posts. $query = new WP_Query( array( 'post_type' => 'post', 'posts_per_page' => -1, // CRITICAL: Unbounded. ) ); // ✅ GOOD: Always set reasonable limits. $query = new WP_Query( array( 'post_type' => 'post', 'posts_per_page' => 100, 'no_found_rows' => true, // Skip counting total rows if not paginating. ) ); ``` ### Using query_posts() (CRITICAL) `query_posts()` replaces the main query, breaking pagination and conditional functions. Never use it. ```php // ❌ CRITICAL: Never use query_posts(). query_posts( 'cat=1&posts_per_page=5' ); // Breaks: is_single(), is_page(), pagination, etc. // ✅ GOOD: Use pre_get_posts to modify main query. add_action( 'pre_get_posts', 'prefix_modify_main_query' ); function prefix_modify_main_query( $query ) { if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) { $query->set( 'posts_per_page', 5 ); $query->set( 'cat', 1 ); } } // ✅ GOOD: Use WP_Query for secondary queries. $custom_query = new WP_Query( array( 'cat' => 1, 'posts_per_page' => 5, ) ); ``` ### Disabling Query Cache (WARNING) Setting `cache_results => false` prevents WordPress from caching query results. ```php // ❌ BAD: Disables query result caching. $query = new WP_Query( array( 'post_type' => 'post', 'cache_results' => false, // Forces fresh DB query every time. ) ); // ✅ GOOD: Let WordPress cache results (default behavior). $query = new WP_Query( array( 'post_type' => 'post', // cache_results defaults to true. ) ); ``` ### Missing WHERE Clause (CRITICAL) Falsy values cast to int become 0, removing the WHERE clause entirely. ```php // ❌ BAD: If $post_id is false/null, this becomes p=0 (no filter!). $post_id = get_some_id_that_might_fail(); // Returns false. $query = new WP_Query( array( 'p' => intval( $post_id ), // intval( false ) = 0. 'posts_per_page' => -1, ) ); // Result: SELECT * FROM wp_posts WHERE 1=1... (returns ALL posts). // ✅ GOOD: Validate before querying. $post_id = get_some_id_that_might_fail(); if ( $post_id ) { $query = new WP_Query( array( 'p' => $post_id, ) ); } ``` ### LIKE with Leading Wildcard (WARNING) Leading wildcards force full table scans - indexes cannot be used. ```php // ❌ BAD: Full table scan. $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s", '%search%' ) ); // ✅ BETTER: Trailing wildcard can use index. $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s", 'search%' ) ); // ✅ BEST: Offload to ElasticSearch for full-text search. // Use ElasticPress plugin for search queries. ``` ### NOT IN Queries (WARNING) `post__not_in` and `NOT IN` clauses scale poorly with large exclusion lists. A better approach is filtering in PHP instead. ```php // ❌ BAD: Slow with many IDs - creates expensive SQL. $query = new WP_Query( array( 'post__not_in' => $hundreds_of_ids, // Each ID checked per row. 'posts_per_page' => 10, ) ); // ✅ GOOD: Use positive filtering when possible. $query = new WP_Query( array( 'post__in' => $desired_ids, 'posts_per_page' => 10, 'orderby' => 'post__in', ) ); // ✅ BETTER: Filter in PHP after fetching extra posts. $posts_to_exclude = array( 1, 2, 3, 4, 5 ); $query = new WP_Query( array( 'posts_per_page' => 10 + count( $posts_to_exclude ), // Fetch extra. 'no_found_rows' => true, ) ); $count = 0; while ( $query->have_posts() && $count < 10 ) { $query->the_post(); if ( in_array( get_the_ID(), $posts_to_exclude, true ) ) { continue; // Skip excluded posts. } // Process post. $count++; } wp_reset_postdata(); ``` ### Over-use of Taxonomies (WARNING) Include child terms multiplies query complexity. ```php // ❌ BAD: Queries all child categories too. $query = new WP_Query( array( 'cat' => 6, // include_children defaults to true. ) ); // ✅ GOOD: Exclude children when not needed. $query = new WP_Query( array( 'cat' => 6, 'include_children' => false, ) ); ``` ### Missing Date Limits (INFO) Mature sites accumulate millions of posts over years. ```php // ❌ BAD: Scans entire posts table. $query = new WP_Query( array( 'category_name' => 'news', 'posts_per_page' => 10, ) ); // ✅ GOOD: Limit to recent content. $query = new WP_Query( array( 'category_name' => 'news', 'posts_per_page' => 10, 'date_query' => array( array( 'after' => '3 months ago', ), ), ) ); ``` ## Hooks & Actions Anti-Patterns ### Expensive Code on Every Request (WARNING) Code hooked to `init`, `wp_loaded`, or `plugins_loaded` runs on EVERY request. ```php // ❌ BAD: Runs on every page load. add_action( 'init', 'prefix_fetch_external_data_bad' ); function prefix_fetch_external_data_bad() { $data = wp_remote_get( 'https://api.example.com/data' ); // HTTP call every request! } // ✅ GOOD: Use transients or object cache. add_action( 'init', 'prefix_fetch_external_data_good' ); function prefix_fetch_external_data_good() { $data = get_transient( 'prefix_external_data' ); if ( false === $data ) { $response = wp_remote_get( 'https://api.example.com/data' ); if ( ! is_wp_error( $response ) ) { $data = wp_remote_retrieve_body( $response ); set_transient( 'prefix_external_data', $data, HOUR_IN_SECONDS ); } } } ``` ### Database Writes on Page Load (CRITICAL) INSERT/UPDATE on every request causes database contention and binlog growth. ```php // ❌ CRITICAL: DB write on every page view. add_action( 'wp_head', 'prefix_track_views_bad' ); function prefix_track_views_bad() { $views = get_option( 'prefix_page_views', 0 ); update_option( 'prefix_page_views', $views + 1 ); } // ✅ GOOD: Batch via object cache, flush periodically via cron. add_action( 'shutdown', 'prefix_track_views_good' ); function prefix_track_views_good() { wp_cache_incr( 'prefix_page_views_buffer', 1, 'counters' ); } // Cron job flushes buffer to database. add_action( 'prefix_flush_view_counts', 'prefix_flush_views_to_db' ); function prefix_flush_views_to_db() { $buffer = wp_cache_get( 'prefix_page_views_buffer', 'counters' ); if ( $buffer ) { $current = get_option( 'prefix_page_views', 0 ); update_option( 'prefix_page_views', $current + $buffer ); wp_cache_delete( 'prefix_page_views_buffer', 'counters' ); } } ``` ### Inefficient Hook Placement (WARNING) Running code in hooks that fire when not needed. ```php // ❌ BAD: Runs on admin AND frontend. add_action( 'init', 'prefix_frontend_only_function' ); // ✅ GOOD: Conditional execution. add_action( 'init', 'prefix_maybe_run_frontend_code' ); function prefix_maybe_run_frontend_code() { if ( is_admin() ) { return; } prefix_frontend_only_function(); } // ✅ BETTER: Use appropriate hook that only fires on frontend. add_action( 'template_redirect', 'prefix_frontend_only_function' ); ``` ### Excessive Hook Callbacks (WARNING) Many callbacks on the same hook add function call overhead. ```php // ❌ BAD: 50 separate callbacks. for ( $i = 0; $i < 50; $i++ ) { add_filter( 'the_content', "prefix_filter_{$i}" ); } // ✅ GOOD: Consolidate into single callback. add_filter( 'the_content', 'prefix_process_content' ); function prefix_process_content( $content ) { // All transformations in one place. $content = prefix_transform_links( $content ); $content = prefix_add_schema( $content ); $content = prefix_lazy_load_images( $content ); return $content; } ``` ## AJAX & External Request Anti-Patterns ### Using admin-ajax.php (WARNING) admin-ajax.php loads full WordPress, including `admin_init` hooks. ```php // ❌ BAD: Full WP bootstrap for simple data. add_action( 'wp_ajax_nopriv_prefix_get_posts', 'prefix_ajax_handler' ); function prefix_ajax_handler() { // Handle AJAX request. wp_send_json_success( $data ); } // ✅ GOOD: Use REST API - leaner bootstrap. add_action( 'rest_api_init', 'prefix_register_rest_routes' ); function prefix_register_rest_routes() { register_rest_route( 'prefix/v1', '/posts', array( 'methods' => 'GET', 'callback' => 'prefix_rest_get_posts', 'permission_callback' => '__return_true', ) ); } function prefix_rest_get_posts( $request ) { // Handle REST request. return rest_ensure_response( $data ); } ``` ### POST for Read Operations (WARNING) POST requests bypass page cache, causing unnecessary server load. ```javascript // ❌ BAD: POST bypasses cache $.post(ajaxurl, { action: 'get_items' }, callback); // ✅ GOOD: GET requests can be cached $.get('/wp-json/myapp/v1/items', callback); ``` ### AJAX Polling (CRITICAL) Polling creates sustained uncached load - effectively a self-DDoS. ```javascript // ❌ CRITICAL: Self-DDoS setInterval(() => { fetch('/wp-json/myapp/v1/updates'); }, 5000); // Every 5 seconds per user! // ✅ GOOD: Use WebSockets, SSE, or long-polling with backoff // ✅ GOOD: Poll less frequently with exponential backoff // ✅ BEST: Push notifications instead of polling ``` ### Uncached External HTTP (WARNING) Synchronous HTTP calls block page generation. ```php // ❌ BAD: Blocks page render. function prefix_get_weather_widget() { $response = wp_remote_get( 'https://api.weather.com/current' ); return prefix_process_weather( $response ); } // ✅ GOOD: Cache external responses. function prefix_get_weather_widget_cached() { $weather = wp_cache_get( 'prefix_weather_data', 'external_api' ); if ( false === $weather ) { $response = wp_remote_get( 'https://api.weather.com/current', array( 'timeout' => 2 ) ); if ( ! is_wp_error( $response ) ) { $weather = wp_remote_retrieve_body( $response ); wp_cache_set( 'prefix_weather_data', $weather, 'external_api', 300 ); } } return prefix_process_weather( $weather ); } // ✅ BEST: Fetch via cron, display from cache. add_action( 'prefix_fetch_weather', 'prefix_cron_fetch_weather' ); function prefix_cron_fetch_weather() { $response = wp_remote_get( 'https://api.weather.com/current' ); if ( ! is_wp_error( $response ) ) { $weather = wp_remote_retrieve_body( $response ); wp_cache_set( 'prefix_weather_data', $weather, 'external_api', HOUR_IN_SECONDS ); } } ``` ## Template Anti-Patterns ### Over-use of get_template_part (WARNING) Each template part requires file system access and additional processing. ```php // ❌ BAD: Template part called 50 times in loop. while ( have_posts() ) { the_post(); get_template_part( 'partials/card' ); // File access each iteration. } // ✅ GOOD: Use template part with data passing (WordPress 5.5+). while ( have_posts() ) { the_post(); get_template_part( 'partials/card', null, array( 'post_id' => get_the_ID(), 'title' => get_the_title(), ) ); } // ✅ ALTERNATIVE: Cache rendered output for identical partials. $card_cache = array(); while ( have_posts() ) { the_post(); $post_id = get_the_ID(); if ( ! isset( $card_cache[ $post_id ] ) ) { ob_start(); get_template_part( 'partials/card' ); $card_cache[ $post_id ] = ob_get_clean(); } echo $card_cache[ $post_id ]; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ``` ### N+1 Query Problem (CRITICAL) Querying inside loops multiplies database calls. ```php // ❌ CRITICAL: 1 query per post. while ( have_posts() ) { the_post(); $author = get_user_by( 'id', get_the_author_meta( 'ID' ) ); // Query per post! $meta = get_post_meta( get_the_ID(), 'views', true ); // Another query per post! } // ✅ GOOD: Prime caches before the loop. $post_ids = wp_list_pluck( $query->posts, 'ID' ); update_postmeta_cache( $post_ids ); // Single query for all meta. while ( have_posts() ) { the_post(); // Now get_post_meta() uses cached data - no additional queries. $meta = get_post_meta( get_the_ID(), 'views', true ); } ``` ## PHP Code Anti-Patterns ### in_array() Without Strict Comparison (WARNING) `in_array()` has O(n) complexity - at scale, use associative array with `isset()` for O(1) lookups. ```php // ❌ BAD: O(n) lookup - slow with large arrays. $allowed = array( 'foo', 'bar', 'baz' ); if ( in_array( $value, $allowed ) ) { // Process. } // ❌ ALSO BAD: Missing strict comparison (type coercion issues). if ( in_array( $value, $allowed ) ) { // 0 == 'foo' is true due to type juggling! } // ✅ GOOD: O(1) lookup with isset(). $allowed = array( 'foo' => true, 'bar' => true, 'baz' => true, ); if ( isset( $allowed[ $value ] ) ) { // Process. } // ✅ ACCEPTABLE: in_array() with strict comparison for small arrays. if ( in_array( $value, $allowed, true ) ) { // Third parameter enables strict type checking. } ``` ### Heredoc/Nowdoc Syntax (WARNING) Heredoc/nowdoc prevents late escaping - escape data at point of output, not before. ```php // ❌ BAD: Can't escape inside heredoc. $html = <<$user_content HTML; // XSS vulnerability - $user_content not escaped. // ✅ GOOD: Late escaping at output. printf( '
%s
', esc_attr( $class ), esc_html( $user_content ) ); // ✅ GOOD: Use template part for complex HTML. get_template_part( 'partials/card', null, array( 'class' => $class, 'content' => $user_content, ) ); ``` ### Storing Large Data in Options (WARNING) The wp_options table should stay lean. Best practice: under 500 rows, autoloaded data under 1MB total. Large autoloaded options slow every page load. ```php // ❌ BAD: Storing HTML/large data in options. update_option( 'prefix_plugin_cache', $huge_html_string ); // ✅ GOOD: Store IDs, fetch data on demand. update_option( 'prefix_plugin_post_ids', array( 1, 2, 3 ) ); // Small data. // ✅ GOOD: Use transients with expiration for cache-like data. set_transient( 'prefix_cache', $data, HOUR_IN_SECONDS ); // ✅ GOOD: Use object cache for large frequently-accessed data. wp_cache_set( 'prefix_data', $large_data, 'prefix_plugin', HOUR_IN_SECONDS ); ``` ## Options & Transients Anti-Patterns ### Large Autoloaded Options (WARNING) All autoloaded options load on every request into `alloptions` cache. ```php // ❌ BAD: Large data autoloaded. add_option( 'prefix_plugin_log', $massive_array ); // autoload defaults to 'yes'. // ✅ GOOD: Disable autoload for large/infrequent data. add_option( 'prefix_plugin_log', $massive_array, '', 'no' ); // ✅ GOOD: Use update_option with explicit autoload (WordPress 4.2+). update_option( 'prefix_plugin_log', $data, false ); // false = no autoload. ``` ### Transients in Database (WARNING) Without object cache, transients bloat wp_options table. ```php // ❌ BAD on hosts without persistent object cache: DB bloat. set_transient( 'prefix_data', $data, DAY_IN_SECONDS ); // ✅ GOOD: Check for object cache availability. if ( wp_using_ext_object_cache() ) { set_transient( 'prefix_data', $data, DAY_IN_SECONDS ); } else { // Use file cache or skip caching on shared hosting. } ``` ## WP Cron Anti-Patterns ### Default WP Cron Behavior (WARNING) By default, WP Cron runs on page requests, adding latency. ```php // ❌ BAD: Cron tasks run during user requests (default behavior). // ✅ GOOD: Disable WP Cron and use server cron. // In wp-config.php: define( 'DISABLE_WP_CRON', true ); // Server crontab: // * * * * * cd /path/to/wp && wp cron event run --due-now ``` ## Uncached Function Calls ### Functions That Need Caching (WARNING) These WordPress core functions query the database on every call without caching. At scale, they cause significant performance issues. | Function | Issue | Solution | |----------|-------|----------| | `url_to_postid()` | Full posts table scan | Wrap with object cache | | `attachment_url_to_postid()` | Expensive meta lookup | Wrap with object cache | | `count_user_posts()` | COUNT query per call | Cache result per user | | `get_adjacent_post()` | Complex query | Cache or avoid in loops | | `wp_oembed_get()` | External HTTP + parsing | Cache with transient | | `wp_old_slug_redirect()` | Meta table lookup | Cache result | | `file_get_contents()` | Filesystem/HTTP call | Cache external content | ### Generic Caching Wrapper Pattern ```php /** * Cached version of url_to_postid() - works on any WordPress installation. * * @param string $url The URL to look up. * @return int Post ID, or 0 if not found. */ function prefix_cached_url_to_postid( $url ) { $cache_key = 'url_to_postid_' . md5( $url ); $post_id = wp_cache_get( $cache_key, 'url_lookups' ); if ( false === $post_id ) { $post_id = url_to_postid( $url ); wp_cache_set( $cache_key, $post_id, 'url_lookups', HOUR_IN_SECONDS ); } return $post_id; } /** * Cached version of count_user_posts(). * * @param int $user_id User ID. * @param string $post_type Post type to count. * @return int Number of posts. */ function prefix_cached_count_user_posts( $user_id, $post_type = 'post' ) { $cache_key = 'user_post_count_' . $user_id . '_' . $post_type; $count = wp_cache_get( $cache_key, 'user_counts' ); if ( false === $count ) { $count = count_user_posts( $user_id, $post_type ); wp_cache_set( $cache_key, $count, 'user_counts', HOUR_IN_SECONDS ); } return $count; } /** * Reusable wrapper for any expensive function call. * * @param string $cache_key Unique cache key. * @param callable $callback Function to call if cache miss. * @param string $group Cache group. * @param int $ttl Time to live in seconds. * @return mixed Cached or fresh result. */ function prefix_cached_call( $cache_key, $callback, $group = '', $ttl = 3600 ) { $result = wp_cache_get( $cache_key, $group ); if ( false === $result ) { $result = call_user_func( $callback ); wp_cache_set( $cache_key, $result, $group, $ttl ); } return $result; } ``` ### WordPress VIP Platform Helpers On WordPress VIP, use the platform's pre-built cached helper functions instead of writing your own wrappers: ```php // ❌ AVOID on VIP: Uncached core functions. $post_id = url_to_postid( $url ); $attach_id = attachment_url_to_postid( $image_url ); $post_count = count_user_posts( $user_id ); $prev_post = get_adjacent_post( false, '', true ); $embed_html = wp_oembed_get( $video_url ); $response = wp_remote_get( $api_url ); // ✅ USE on VIP: Cached platform alternatives. $post_id = wpcom_vip_url_to_postid( $url ); $attach_id = wpcom_vip_attachment_url_to_postid( $image_url ); $post_count = wpcom_vip_count_user_posts( $user_id ); $prev_post = wpcom_vip_get_adjacent_post( false, '', true ); $embed_html = wpcom_vip_wp_oembed_get( $video_url ); $response = vip_safe_wp_remote_get( $api_url ); // VIP-specific: Safe remote request with built-in fallback and caching. $response = vip_safe_wp_remote_get( $api_url, array( 'fallback_value' => '', // Return this on failure. 'threshold' => 3, // Failures before fallback. 'timeout' => 2, // Request timeout in seconds. ) ); ``` ### Platform Guidance | Platform | Recommendation | |----------|----------------| | **WordPress VIP** | Use `wpcom_vip_*` helpers - they handle caching, fallbacks, and edge cases | | **WP Engine, Pantheon, etc.** | Check host documentation for platform-specific optimizations | | **Self-hosted with object cache** | Use the generic caching wrapper pattern above | | **Shared hosting (no object cache)** | Use transients, but be aware they fall back to database storage | ## External API Anti-Patterns ### Plugin-Initiated PHP Sessions (CRITICAL) Plugins that call `session_start()` on frontend requests make the entire site uncacheable. ```php // ❌ CRITICAL: This single line can disable page caching site-wide session_start(); // Detection: Search codebase for session_start() grep -r "session_start" wp-content/plugins/ wp-content/themes/ // ✅ SOLUTION: Use cookies or wp_cache for non-sensitive data // ✅ SOLUTION: Only use sessions for logged-in users if (is_user_logged_in()) { session_start(); } ``` ### Query Parameter Cache Busting (WARNING) Marketing UTM parameters and tracking IDs create unique URLs, causing cache misses. ```php // ❌ BAD: Each unique URL = separate cache entry // https://example.com/?utm_source=facebook&utm_campaign=summer&fbclid=abc123 // https://example.com/?utm_source=twitter&utm_campaign=summer // Result: Homepage cached hundreds of times with different params // ✅ SOLUTION: Configure CDN to ignore/strip marketing params // Cloudflare, Fastly, Varnish can all strip query params before cache lookup // ✅ SOLUTION: Canonical redirect in WordPress add_action('template_redirect', function() { $strip_params = ['utm_source', 'utm_medium', 'utm_campaign', 'fbclid', 'gclid']; $dominated_query = array_diff_key($_GET, array_flip($strip_params)); if (count($_GET) !== count($dominated_query)) { $url = remove_query_arg($strip_params); wp_redirect($url, 301); exit; } }); ``` ### Unnecessary Cookies on Public Pages (WARNING) Setting cookies prevents CDN caching for that visitor. ```php // ❌ BAD: Cookie set on every visit add_action('init', function() { setcookie('visitor_tracking', uniqid(), time() + 86400, '/'); }); // Result: No page caching for any visitor with this cookie // ✅ GOOD: Set tracking cookies via JavaScript (doesn't affect server cache) // ✅ GOOD: Use analytics services instead of custom cookies // ✅ GOOD: If cookie needed, set only on specific actions (not every page) ``` ### Missing Timeout Set (WARNING) Default timeout is 5 seconds - too long for page generation. ```php // ❌ BAD: Default 5 second timeout blocks page render $response = wp_remote_get('https://api.example.com/data'); // ✅ GOOD: Short timeout with fallback $response = wp_remote_get('https://api.example.com/data', [ 'timeout' => 2, // 2 seconds max ]); if (is_wp_error($response)) { // Return cached/default data return get_fallback_data(); } ``` ### No Error Handling (WARNING) API failures can break page output or cause PHP errors. ```php // ❌ BAD: Assumes success $response = wp_remote_get($url); $data = json_decode($response['body']); // ✅ GOOD: Handle failures gracefully $response = wp_remote_get($url, ['timeout' => 2]); if (is_wp_error($response)) { error_log('API Error: ' . $response->get_error_message()); return $cached_fallback; } $code = wp_remote_retrieve_response_code($response); if (200 !== $code) { error_log("API returned status: $code"); return $cached_fallback; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); ``` ### Synchronous API Calls Without Caching (WARNING) Every page load waits for external API response. ```php // ❌ BAD: Blocks every page load function get_weather() { return wp_remote_get('https://api.weather.com/...'); } // ✅ GOOD: Cache responses, fetch via cron function get_weather() { $cached = wp_cache_get('weather_data'); if (false !== $cached) { return $cached; } // Fallback to slightly stale data if API fails $stale = get_transient('weather_data_stale'); $response = wp_remote_get('https://api.weather.com/...', ['timeout' => 2]); if (!is_wp_error($response)) { $data = wp_remote_retrieve_body($response); wp_cache_set('weather_data', $data, '', 300); set_transient('weather_data_stale', $data, DAY_IN_SECONDS); return $data; } return $stale ?: ''; } ``` ## Sitemap Anti-Patterns ### Uncached Dynamic Sitemaps (WARNING) Crawlers can hammer sitemap endpoints, generating expensive queries. ```php // ❌ BAD: Sitemap generated on every request // WordPress core sitemaps can be slow for large archives // ✅ GOOD: Pre-generate and cache sitemaps. // Use msm-sitemaps plugin for large sites. // Or cache sitemap output: function cached_sitemap() { $cache_key = 'sitemap_' . get_query_var('sitemap'); $sitemap = wp_cache_get($cache_key, 'sitemaps'); if (false === $sitemap) { $sitemap = generate_sitemap(); wp_cache_set($cache_key, $sitemap, 'sitemaps', HOUR_IN_SECONDS); } return $sitemap; } ``` ### Including Deep Archives (INFO) Sitemaps for years-old content trigger unnecessary query load. ```php // ✅ GOOD: Exclude old content from sitemaps add_filter('wp_sitemaps_posts_query_args', function($args) { $args['date_query'] = [ 'after' => '2 years ago' ]; return $args; }); // ✅ GOOD: Exclude specific post types add_filter('wp_sitemaps_post_types', function($post_types) { unset($post_types['attachment']); unset($post_types['revision']); return $post_types; }); ``` ## Plugin & Theme Anti-Patterns ### Page Builder Performance (WARNING) Page builder plugins add significant overhead - extra code, database queries, and processing. ```php // ❌ WARNING: Page builders at scale. // - Generate inefficient HTML/CSS. // - Add many database queries per page. // - May compile templates on every request (especially problematic on read-only filesystems). // - Block editor (Gutenberg) is more performant than most page builders. // ✅ RECOMMENDATIONS: // - Use Gutenberg/block editor for new projects. // - Custom code for high-traffic landing pages. // - If using page builders, test query count and generation time. // - Avoid page builders on pages receiving high traffic. ``` ### Infinite Scroll with POST Requests (WARNING) Infinite scroll plugins often use POST requests, bypassing cache entirely. ```php // ❌ BAD: POST request for each scroll (bypasses cache) // As user scrolls, each AJAX request = uncached hit to origin jQuery.post(ajaxurl, { action: 'load_more_posts', page: 2 }); // ✅ GOOD: Use GET requests for infinite scroll (cacheable) jQuery.get('/wp-json/mysite/v1/posts', { page: 2 }); // ✅ GOOD: Pre-render next page URLs that can be cached // /page/2/, /page/3/ etc. are cacheable at CDN level // ✅ GOOD: Implement cache warming for paginated content ``` ## JavaScript Bundle Anti-Patterns ### Full Library Imports (WARNING) Importing entire libraries when only parts are needed bloats JavaScript bundles. ```javascript // ❌ BAD: Imports entire lodash library (~70KB) import _ from 'lodash'; const result = _.map(items, transform); // ✅ GOOD: Import only what you need (~2KB) import map from 'lodash/map'; const result = map(items, transform); // ❌ BAD: Barrel file exports pull in everything // utils/index.js: export * from './heavy-module'; import { smallFunction } from './utils'; // Loads entire utils // ✅ GOOD: Import directly from module import { smallFunction } from './utils/small-module'; ``` ### Missing Script Loading Strategy (INFO) WordPress 6.3+ supports native defer/async for non-blocking script loading. ```php // ❌ BAD: Blocks rendering (default behavior) wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js'); // ✅ GOOD: Defer non-critical scripts (WordPress 6.3+) wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js', [], '1.0.0', [ 'strategy' => 'defer', // or 'async' ]); // ✅ GOOD: Load in footer for older WP versions wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js', [], '1.0.0', true); ``` ### Missing Asset Version Strings (INFO) Without version strings, browser caches may serve stale assets after deployments. ```php // ❌ BAD: No version - cache busting issues wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js'); wp_enqueue_style('my-style', get_template_directory_uri() . '/css/style.css'); // ✅ GOOD: Use theme/plugin version constant define('THEME_VERSION', '1.0.0'); wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js', [], THEME_VERSION); wp_enqueue_style('my-style', get_template_directory_uri() . '/css/style.css', [], THEME_VERSION); // ✅ GOOD: Use file modification time for development wp_enqueue_script('my-script', get_template_directory_uri() . '/js/script.js', [], filemtime(get_template_directory() . '/js/script.js')); ``` ## Block Editor Anti-Patterns ### Too Many Custom Block Styles (WARNING) Each block style creates a preview iframe in the editor, causing severe performance degradation. ```javascript // ❌ BAD: Each style = separate iframe for preview registerBlockStyle('core/group', { name: 'green-dots', label: 'Green Dots' }); registerBlockStyle('core/group', { name: 'blue-waves', label: 'Blue Waves' }); registerBlockStyle('core/group', { name: 'red-stripes', label: 'Red Stripes' }); // 10+ styles = editor becomes unusable // ✅ GOOD: Use custom attributes with block filters import { addFilter } from '@wordpress/hooks'; addFilter('blocks.registerBlockType', 'namespace/bg-patterns', (settings, name) => { if (name !== 'core/group') return settings; return { ...settings, attributes: { ...settings.attributes, backgroundPattern: { type: 'string', default: '' } } }; }); // Add UI via BlockEdit filter, style via render_block filter ``` ### Re-sanitizing InnerBlocks Content (WARNING) InnerBlocks content is already sanitized - running wp_kses_post breaks embeds and core functionality. ```php // ❌ BAD: Breaks embeds, iframes, and other allowed content function render_my_block($attributes, $content) { return '
' . wp_kses_post($content) . '
'; } // ✅ GOOD: InnerBlocks content is pre-sanitized function render_my_block($attributes, $content) { return '
' . $content . '
'; } // ✅ GOOD: Escape attributes, not InnerBlocks content function render_my_block($attributes, $content) { $class = esc_attr($attributes['className'] ?? ''); return '
' . $content . '
'; } ``` ### Static Blocks for Client Builds (INFO) Static blocks store markup in the database, requiring deprecations for any design changes. ```php // ❌ PROBLEMATIC: Static block - markup stored in DB // Any HTML change requires deprecation handler or manual re-save of all posts // ✅ GOOD: Dynamic block - only attributes stored, markup rendered on request register_block_type('namespace/my-block', [ 'render_callback' => 'render_my_block', // Dynamic rendering 'attributes' => [ 'title' => ['type' => 'string'], ] ]); // Design changes update all instances automatically without re-saving posts ``` ## Redirect Anti-Patterns ### Redirect Loops (CRITICAL) Infinite server-side redirects consume CPU without easy detection. ```php // Debug redirect source using x-redirect-by header add_filter('x_redirect_by', function($x_redirect_by, $status, $location) { // Log for debugging error_log("Redirect to $location by: $x_redirect_by (status: $status)"); // For deep debugging, output stack trace: // error_log(wp_debug_backtrace_summary()); return $x_redirect_by; }, 10, 3); // Always set x_redirect_by when creating redirects wp_redirect($url, 301, 'My Plugin Name'); ``` ### Redirect Chains (WARNING) Multiple sequential redirects add latency and confuse caches. ```php // ❌ BAD: A → B → C → D (chain of redirects) // Each redirect = full HTTP round-trip // ✅ GOOD: Direct redirect A → D // Audit redirects regularly, collapse chains ``` ## Post Meta Anti-Patterns ### Querying meta_value Without Index (WARNING) The `meta_value` column isn't indexed by default - full table scan. ```php // ❌ BAD: Scans entire postmeta table $query = new WP_Query([ 'meta_query' => [ ['key' => 'color', 'value' => 'red'] ] ]); // ✅ BETTER: Use taxonomy for filterable attributes // Register 'color' taxonomy, term 'red' $query = new WP_Query([ 'tax_query' => [ ['taxonomy' => 'color', 'field' => 'slug', 'terms' => 'red'] ] ]); ``` ### Binary Meta Values (WARNING) Checking `meta_value = 'true'` requires scanning all matching keys. ```php // ❌ BAD: Must scan meta_value column $query = new WP_Query([ 'meta_key' => 'is_featured', 'meta_value' => 'true' ]); // ✅ GOOD: Key presence = true, absence = false $query = new WP_Query([ 'meta_key' => 'is_featured', 'meta_compare' => 'EXISTS' ]); // ✅ ALTERNATIVE: Encode value in key name // Instead of: meta_key='category', meta_value='sports' // Use: meta_key='category_sports' (just check EXISTS) ``` ### Excessive Post Meta (INFO) `wp_postmeta` table grows to multiples of `wp_posts` - optimize storage. ```php // ❌ BAD: Storing large data in post meta update_post_meta($id, 'full_api_response', $huge_json); // ✅ GOOD: Store minimal data, fetch details on demand update_post_meta($id, 'api_resource_id', $resource_id); // ❌ BAD: Many separate meta entries update_post_meta($id, 'address_line1', $line1); update_post_meta($id, 'address_line2', $line2); update_post_meta($id, 'address_city', $city); // ... 10 more fields // ✅ GOOD: Serialize related data update_post_meta($id, 'address', [ 'line1' => $line1, 'city' => $city, // ... ]); ```