WordPress REST API Performance: Sparse Fieldsets and Caching Strategies

The WordPress REST API returns full post objects by default — including 40+ fields, embedded author objects, and taxonomy arrays — even when the client only needs the title and permalink. The _fields parameter enables sparse fieldsets to reduce payload size by 80–90%, and combining sparse fieldsets with HTTP caching headers and a Redis-backed response cache eliminates redundant processing entirely for read-heavy endpoints.

Problem: A WordPress REST API response for a post includes dozens of fields — content, meta, links, embedded terms — but a client application only needs four fields, causing unnecessary payload size and serialisation overhead.

Solution: Use sparse fieldsets with the _fields parameter: /wp-json/wp/v2/posts?_fields=id,title,slug,date reduces the response to only the requested fields. Add server-side caching of REST responses with a rest_pre_dispatch filter that checks a Redis or transient cache keyed by the full request URL, including query string. Set Vary: Accept headers appropriately.


The code below adds server-side sparse fieldset enforcement, implements a REST response cache using object cache groups, adds proper Cache-Control and ETag headers for HTTP caching, and shows how to use _fields in JavaScript fetch calls.


get_method() !== 'GET' ) {
        return $result;
    }
    if ( ! empty( $request->get_header( 'authorization' ) ) ) {
        return $result;   // never cache authenticated requests
    }

    $cache_key   = 'rest_' . md5( $request->get_route() . serialize( $request->get_query_params() ) );
    $cache_group = 'rest_api';
    $cached      = wp_cache_get( $cache_key, $cache_group );

    if ( false !== $cached ) {
        return $cached;
    }
    return $result;
}, 10, 3 );

add_filter( 'rest_post_dispatch', function ( WP_HTTP_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_HTTP_Response {
    if ( $request->get_method() !== 'GET' ) {
        return $response;
    }
    if ( $response->get_status() !== 200 ) {
        return $response;
    }
    if ( ! empty( $request->get_header( 'authorization' ) ) ) {
        return $response;
    }

    // Add Cache-Control and ETag headers
    $etag = md5( json_encode( $response->get_data() ) );
    $response->header( 'Cache-Control', 'public, max-age=60, stale-while-revalidate=300' );
    $response->header( 'ETag', '"' . $etag . '"' );

    // Check If-None-Match for 304 response
    $if_none_match = trim( $request->get_header( 'if-none-match' ), '"' );
    if ( $if_none_match === $etag ) {
        return new WP_HTTP_Response( null, 304 );
    }

    // Store in object cache
    $cache_key = 'rest_' . md5( $request->get_route() . serialize( $request->get_query_params() ) );
    wp_cache_set( $cache_key, $response->get_data(), 'rest_api', 60 );

    return $response;
}, 10, 3 );

// 2. Invalidate REST cache on post save
add_action( 'save_post', function ( int $post_id ) {
    wp_cache_flush_group( 'rest_api' );   // requires object cache that supports group flush
} );


// 3. Client-side: use _fields to request only needed data
const BASE = '/wp-json/wp/v2';

// Fetch only id, title, link — reduces ~3KB response to ~150 bytes per post
const posts = await fetch(
    `${BASE}/posts?_fields=id,title,link&per_page=10`
).then( r => r.json() );

// Embed author name inline (avoids second request) + sparse fields
const postsWithAuthor = await fetch(
    `${BASE}/posts?_fields=id,title,link,_embedded&_embed=author&per_page=10`
).then( r => r.json() );

// Access embedded author:
postsWithAuthor.forEach( post => {
    const authorName = post._embedded?.author?.[0]?.name ?? 'Unknown';
    console.log( post.title.rendered, '—', authorName );
} );


NOTE: wp_cache_flush_group() only works with object cache backends that support group-based flushing (Redis with the WP Redis plugin or Memcached with W3TC) — the default WordPress file-based object cache does not support it; on sites without a persistent object cache, use a version token in the cache key (e.g., append get_option('rest_cache_version')) and increment it on save instead.