LESSON 7 โฑ๏ธ 12 min read

Performance Optimization & Best Practices

API Performance Matters

Slow API responses hurt user experience and waste resources. Let's optimize.

Caching Strategies

Transient Caching

Cache expensive queries:

function get_popular_posts_api() {
    $cache_key = 'popular_posts_api';
    $cached = get_transient($cache_key);
    
    if ($cached !== false) {
        return $cached;
    }
    
    // Expensive query
    $posts = new WP_Query([
        'posts_per_page' => 10,
        'meta_key'       => 'views_count',
        'orderby'        => 'meta_value_num',
        'order'          => 'DESC'
    ]);
    
    $data = array_map(function($post) {
        return [
            'id'    => $post->ID,
            'title' => $post->post_title,
            'views' => get_post_meta($post->ID, 'views_count', true)
        ];
    }, $posts->posts);
    
    // Cache for 1 hour
    set_transient($cache_key, $data, HOUR_IN_SECONDS);
    
    return $data;
}

Object Cache

For more persistent caching:

function get_task_stats() {
    $cache_key = 'task_stats';
    $stats = wp_cache_get($cache_key, 'taskmanager');
    
    if ($stats !== false) {
        return $stats;
    }
    
    global $wpdb;
    $stats = $wpdb->get_results(
        "SELECT status, COUNT(*) as count 
         FROM {$wpdb->prefix}tasks 
         GROUP BY status"
    );
    
    wp_cache_set($cache_key, $stats, 'taskmanager', 300);
    
    return $stats;
}

// Invalidate on changes
function invalidate_task_cache() {
    wp_cache_delete('task_stats', 'taskmanager');
    delete_transient('popular_posts_api');
}
add_action('task_created', 'invalidate_task_cache');
add_action('task_updated', 'invalidate_task_cache');

HTTP Caching Headers

Let browsers and CDNs cache responses:

register_rest_route('myplugin/v1', '/public-data', [
    'methods'  => 'GET',
    'callback' => function() {
        $data = get_public_data();
        
        $response = new WP_REST_Response($data);
        
        // Cache for 5 minutes
        $response->header('Cache-Control', 'public, max-age=300');
        
        // ETag for conditional requests
        $etag = md5(serialize($data));
        $response->header('ETag', $etag);
        
        return $response;
    },
    'permission_callback' => '__return_true'
]);

Reducing Response Size

Select Only Needed Fields

// Instead of SELECT *
$tasks = $wpdb->get_results(
    "SELECT id, title, status FROM {$wpdb->prefix}tasks"
);

Filter Response Data

function prepare_task_response($task, $context = 'view') {
    $data = [
        'id'     => $task->id,
        'title'  => $task->title,
        'status' => $task->status
    ];
    
    // Include extra data only when editing
    if ($context === 'edit') {
        $data['created_at'] = $task->created_at;
        $data['created_by'] = $task->created_by;
    }
    
    return $data;
}

Pagination

Never return unlimited results:

'args' => [
    'per_page' => [
        'default' => 10,
        'maximum' => 100,  // Hard limit
        'minimum' => 1
    ]
]

Query Optimization

Use Proper Indexes

-- Add indexes for frequently queried columns
ALTER TABLE wp_tasks ADD INDEX status_idx (status);
ALTER TABLE wp_tasks ADD INDEX created_by_idx (created_by);
ALTER TABLE wp_tasks ADD INDEX created_at_idx (created_at);

Avoid N+1 Queries

// โŒ Bad: N+1 queries
function get_tasks_with_users_bad() {
    $tasks = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}tasks");
    
    foreach ($tasks as &$task) {
        // One query per task!
        $task->user = get_userdata($task->created_by);
    }
    
    return $tasks;
}

// โœ… Good: Single join query
function get_tasks_with_users_good() {
    global $wpdb;
    
    return $wpdb->get_results("
        SELECT t.*, u.display_name as user_name, u.user_email
        FROM {$wpdb->prefix}tasks t
        LEFT JOIN {$wpdb->users} u ON t.created_by = u.ID
        ORDER BY t.created_at DESC
        LIMIT 20
    ");
}

Rate Limiting

Prevent abuse:

function check_rate_limit(WP_REST_Request $request) {
    $user_id = get_current_user_id();
    $ip = $_SERVER['REMOTE_ADDR'];
    $key = "rate_limit_{$user_id}_{$ip}";
    
    $requests = get_transient($key) ?: 0;
    
    if ($requests >= 100) {  // 100 requests per minute
        return new WP_Error(
            'rate_limited',
            'Too many requests. Please try again later.',
            ['status' => 429]
        );
    }
    
    set_transient($key, $requests + 1, MINUTE_IN_SECONDS);
    
    return true;
}

// Apply to endpoint
register_rest_route('myplugin/v1', '/expensive-operation', [
    'methods'             => 'POST',
    'callback'            => 'do_expensive_operation',
    'permission_callback' => 'check_rate_limit'
]);

Best Practices Summary

PracticeWhy
Use proper HTTP methodsSemantic clarity, caching
Return appropriate status codesClient error handling
Validate all inputSecurity
Sanitize outputXSS prevention
Paginate collectionsPerformance
Version your APIBackward compatibility
Document endpointsDeveloper experience
Use consistent namingsnake_case for JSON keys

Security Checklist

// โœ… Always validate & sanitize
$title = sanitize_text_field($request->get_param('title'));

// โœ… Use proper capabilities
if (!current_user_can('manage_options')) {
    return new WP_Error('forbidden', 'Access denied', ['status' => 403]);
}

// โœ… Verify ownership
if ($task->created_by !== get_current_user_id()) {
    return new WP_Error('forbidden', 'Not your task', ['status' => 403]);
}

// โœ… Escape output
return [
    'title' => esc_html($task->title),
    'url'   => esc_url($task->url)
];

Monitoring

Log slow queries and errors:

add_action('rest_request_after_callbacks', function($response, $handler, $request) {
    global $wpdb;
    
    // Log slow requests
    $time = timer_stop();
    if ($time > 1.0) {  // Over 1 second
        error_log(sprintf(
            'Slow REST API: %s %s took %.2fs',
            $request->get_method(),
            $request->get_route(),
            $time
        ));
    }
    
    // Log DB query count
    if ($wpdb->num_queries > 50) {
        error_log(sprintf(
            'High query count: %s had %d queries',
            $request->get_route(),
            $wpdb->num_queries
        ));
    }
    
    return $response;
}, 10, 3);

Congratulations!

You've completed the WordPress REST API Mastery tutorial!

What You've Learned

  • โœ… REST API architecture and endpoints
  • โœ… Advanced querying with filters and embedding
  • โœ… Multiple authentication methods
  • โœ… Create, Update, Delete operations
  • โœ… Building custom endpoints
  • โœ… JavaScript frontend integration
  • โœ… Performance optimization

Next Steps

  1. Build a headless app with Next.js or React
  2. Create a mobile app using React Native
  3. Integrate external services like CRMs or email platforms
  4. Build custom Gutenberg blocks that use the API
๐ŸŽฏ Tutorial Complete! You're now proficient in the WordPress REST API and can build powerful integrations.
๐ŸŽ‰

Tutorial Complete!

You've finished all lessons in this series.

โ† Back to Tutorial Overview