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.