At WordCamp US 2025 this year, I’m presenting a talk called “The Site Speed Frontier with Performance Lab and Beyond” with the following description:
The Core Performance team has been incubating enhancements for WordPress through the Performance Lab plugin. These have been available for a few years now; some have been merged into core (e.g. Speculative Loading) while others are more experimental and remain in testing (e.g. Optimization Detective). This talk will look at how these performance plugins impact the speed of a stock WordPress site running the Twenty Twenty-Five default theme, using Core Web Vitals benchmarks and Lighthouse scores. It will also look at how the theme’s performance can be further tuned, including the use of core patches proposed for the next major release (also available in plugin form to leverage today) to further accelerate the loading of pages to improve the user experience of site visitors.
Here’s the livestream recording:
And here are my slides as well:
And what follows is my talk in blog post form, greatly expanded with a lot more details than I had time to share during my talk.
Table of contents:
Performance Lab
I’ve been a WordPress core committer for over 10 years, and since Spring 2023 I’ve been heavily involved on the Core Performance Team. In addition to contributing patches directly to the WordPress core codebase, we also develop new performance optimizations in the form of feature plugins. We use our Performance Lab plugin as a way to collect the feature plugins we’re currently working on to facilitate discovery:
Most of these performance feature plugins are developed in the WordPress/performance monorepo on GitHub. In the same way as the Gutenberg plugin serves as a way to develop new editor features, the Performance Lab plugin is a way we incubate new performance features. It allows us to get feedback from users and test the impact prior to being proposed for merging into a new release of WordPress core when it gets rolled out to ~43% of the web.
Case Study: Twenty Twenty-Five
The default theme for the current version of WordPress is Twenty Twenty- Five. Default themes in core basically encapsulate the latest and greatest in what WordPress has to offer in terms of features and performance. Indeed, block themes are generally faster than classic themes (especially with page caching) for a few reasons, including:
- Scripts and styles are selectively loaded based on whether their blocks actually used on the page.
- Blocks are more likely to use the Interactivity API which involves deferred script modules (which don’t block rendering) and server-side rendering.
So the Twenty Twenty-Five theme should be very fast, and indeed it is. But with Performance Lab features (and beyond), it can be made even faster.
Your mileage will vary with other themes, either having an even greater impact or a lesser one. Every site is unique (hopefully!) and so the impact of optimizations depends on a page’s contents, how a theme is built, and which plugins are active. But in this post, I’ll show the impact of the optimizations in various page layouts of the Twenty Twenty-Five Theme.
Performance Testing Methodology
Perhaps the most popular way to analyze the performance of a webpage is to use Lighthouse, either in Chrome DevTools or via the bottom half of PageSpeed Insights. Lighthouse allows you to test pages either as a desktop or mobile device, emulating the viewport, CPU, and connection speed. Lighthouse is an important tool to get a sense of a page’s performance, but it has limitations. It captures data from a single page load on a simulated device. There is often variability in the results, and it also doesn’t reflect the experience of real users which is what you’d get from Real User Monitoring (RUM), such as in the Chrome User Experience Report (CrUX)—shown in the first section of PageSpeed Insights. Lighthouse provides simulated lab data whereas CrUX provides real field data, which is more accurate. Nevertheless, field data can take a long time to collect and it can be difficult to do A/B tests at scale to capture the before/after performance impacts. That said, CrUX is definitely used to track the performance of new WordPress releases overall, as Felix Arntz shared the WordPress performance impact on Core Web Vitals in 2023. Felix also wrote up how to conduct WordPress performance research in the field.
For the purposes of measuring the impact of performance optimizations here, lab data will be more practical because the results are available in real time, without having to wait for real users to provide field data. And I’m interested in relative performance impacts, not necessarily absolute ones.
Running a Lighthouse audit before and after a Performance Lab feature plugin is active is a way to measure the impact of the optimization. However, given variability in the results, it can be difficult to be certain of the improvement. A Lighthouse audit may no longer flag an area for improvement, but the overall Lighthouse score may be unchanged. Indeed, even a Lighthouse score of 100 doesn’t mean the page performance is “perfect”. As found by Brendan Kenny:
Of the pages that got a 90+ in Lighthouse in September [2021], 43% didn’t meet one or more CWV threshold.

A great score—even 100—doesn’t mean there still isn’t a lot of room for improvement!
Largest Contentful Paint
One of the main components in calculating Lighthouse’s Performance score is the Largest Contentful Paint (LCP) metric of Core Web Vitals (CWV). LCP metric is weighted at 25% of the total Lighthouse score. As noted in how how Lighthouse scores are determined:
The metric value for LCP represents the time duration between the user initiating the page load and the page rendering its primary content. Based on real website data, top-performing sites render LCP in about 1,220ms, so that metric value is mapped to a score of 99.
A 1.2 second LCP is 12 times slower than a 100 ms LCP, where 100 ms is a proposed threshold for the user to perceive a reaction as being instantaneous. Nevertheless, a “good” LCP value is 2.5 seconds and below:
The LCP metric can be further subdivided, with the first part represented by the Time To First Byte (TTFB) metric. The longer it takes the server to respond with the generated HTML document, the more this will hurt the LCP metric. A slow TTFB means you are less likely to have a good LCP. If a site has a 1.5-second TTFB which needs improvement, then this leaves only 1 second for the LCP element to be rendered to get a good LCP metric. A good TTFB is considered to be about half that, at 800 ms and below:
Unfortunately, in looking at HTTP Archive’s Tech Report, as of July 2025, only 31% of desktop clients visiting WordPress sites experience a good TTFB, whereas it’s just 24% for mobile clients. This means it is all too likely that a 1.5-second TTFB is the norm for WordPress sites. This is in part what contributes to WordPress lagging behind most other CMSes for the LCP metric on mobile and on desktop, even about 10% below the average on all measured sites.
In comparison with the other CWV metrics—Cumulative Layout Shift (CLS) and Interaction to Next Paint (INP)—WordPress is doing worse in terms of LCP, as evident in the metric passing rates from the following reports on HTTP Archive:
Therefore, improving LCP remains the most important focus for performance optimizations in WordPress. So in this post I’ll focus on the LCP impact for the plugins featured in Performance Lab and some other changes proposed for WordPress 6.9.
Benchmarking
Because the Lighthouse score is variable and a 100 score merely reflects a “good” LCP, evaluating the performance benefit of an optimization requires measuring the LCP metric itself. Due to the variability in the metric, it’s important to obtain the median value of the LCP over many measurements. By capturing the median LCP value before and after an optimization is applied, the relative impact on performance can be measured.
The tool I use for benchmarking LCP is in the GoogleChromeLabs/wpp-research repo, which my team developed when I was at Google. Specifically, I use the benchmark-web-vitals
command which includes the ability to emulate mobile and desktop devices, network connections, and CPU speeds.
Here’s an example command I use to benchmark two URLs emulating a mobile device on a Fast 4G connection, and compare their results:
npm run research -- benchmark-web-vitals \
--url="http://localhost/?enable_plugins=none" \
--url="http://localhost/?enable_plugins=foo" \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
Helper mu-plugin to override active plugins via query vars
I threw this together to help me with benchmarking so that I didn’t have to manually activate/deactivate plugins constantly.
<?php
/**
* Plugin Name: Active Plugins Override
*/
namespace ActivePluginsOverride;
function get_always_active_plugins(): array {
return array(
'user-switching/user-switching.php'
);
}
add_filter(
'option_active_plugins',
static function ( $plugins ) {
return array_unique( array_merge( $plugins, get_always_active_plugins() ) );
},
100
);
if ( isset( $_GET['disable_all_plugins'] ) || ( isset( $_GET['enable_plugins'] ) && $_GET['enable_plugins'] === 'none' ) ) {
add_filter( 'option_active_plugins', '__return_empty_array' );
}
if ( isset( $_GET['disable_plugins'] ) ) {
if ( is_array( $_GET['disable_plugins'] ) ) {
$disable_plugins = $_GET['disable_plugins'];
} else {
$disable_plugins = explode( ',', $_GET['disable_plugins'] );
}
add_filter(
'option_active_plugins',
function ( $active_plugins ) use ( $disable_plugins ) {
return array_merge(
array_filter(
$active_plugins,
function ( $active_plugin ) use ( $disable_plugins ) {
$slug = strtok( $active_plugin, '/' );
return ! in_array( $slug, $disable_plugins );
}
),
get_always_active_plugins()
);
}
);
}
if ( isset( $_GET['enable_plugins'] ) ) {
if ( is_array( $_GET['enable_plugins'] ) ) {
$enable_plugins = $_GET['enable_plugins'];
} else {
$enable_plugins = explode( ',', $_GET['enable_plugins'] );
}
if ( count( array_intersect( $enable_plugins, array( 'embed-optimizer', 'image-prioritizer' ) ) ) > 0 ) {
$enable_plugins[] = 'optimization-detective';
$enable_plugins[] = 'od-admin-ui';
}
add_filter(
'option_active_plugins',
function ( $active_plugins ) use ( $enable_plugins ) {
return array_filter(
$active_plugins,
function ( $active_plugin ) use ( $enable_plugins ) {
$slug = strtok( $active_plugin, '/' );
return in_array( $slug, $enable_plugins );
}
);
}
);
}
Code language: PHP (php)
This results in a table like the following, showing the median metrics for the number of requests to both URLs:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 436.1 | 438.6 | +2.5 | +0.6% |
LCP | 915.3 | 690.9 | -224.4 | -24.5% |
TTFB | 50.8 | 50.6 | -0.2 | -0.3% |
LCP-TTFB | 865.6 | 638.3 | -227.3 | -26.3% |
In this example, the LCP improved by ~25% by enabling the “foo” plugin, which is exactly the kind of performance improvement we’re looking for on the Core Performance Team. Note this “LCP-TTFB” metric is simply the LCP metric minus the TTFB metric; this allows for measuring the client-side contributions to LCP by discounting any server-side variability in generating the response. The LCP-TTFB metric is important considering the lack of page caching on a local environment, and that certain optimizations may increase TTFB when page caching is not involved. For WordPress to scale, it’s important to have some page caching layer in place.
Analyzing Optimization Impact on LCP
I’m going to analyze the impact of the following feature plugins featured in Performance Lab:
- Image Placeholders
- Modern Image Formats
- Enhanced Responsive Images
- Image Prioritizer
- Speculative Loading
- View Transitions
I’m not covering Performant Translations since it was mostly merged into core as of 6.5. I’m also not covering Embed Optimizer since it primarily helps with INP by lazy-loading and CLS by reserving space for resizing embeds; the LCP improvement is difficult to measure for embeds that appear in the initial viewport given their cross-origin nature. Lastly, I’m not covering Web Worker Offloading since it is quite experimental and it is only related to INP. However, I am going to cover enhancements beyond Performance Lab being targeted for WordPress 6.9:
- No-cache BFCache (actually, brand new to Performance Lab)
- Script Module Deprioritization
- Minified CSS Inlining
The first four Performance Lab feature plugins are all related to images. In focusing on improving the LCP metric, this makes sense because images are the LCP element 73.3% of the time on mobile and 83.3% of the time on desktop, according to Web Almanac 2024:

Image Placeholders
The Image Placeholders plugin, originally called “Dominant Color Images”, adds a non-transparent image’s dominant color as the background color. This improves the perceived page loading experience by showing something sooner, rather than just a blank spot on the page.
Instead of this:

With the plugin active (and the media regenerated), the following is the result:

The visual impact that this plugin has on the loading of the page is that there is a brown rectangle serving as a placeholder for where the user can expect an image to load.
However, when benchmarking the web vitals, there is no improvement in LCP. In fact, there even appears to be a slight regression:
Before | After | Diff (ms) | Diff (%) | |
---|---|---|---|---|
FCP | 437.2 | 439.5 | +2.3 | +0.5% |
LCP | 610.8 | 613.5 | +2.7 | +0.4% |
TTFB | 44.2 | 44.0 | -0.2 | -0.5% |
LCP-TTFB | 566.3 | 568.1 | +1.8 | +0.3% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/2025/07/30/bison-featured-image/?enable_plugins=none" \
--url="https://wcus-perf-talk-demo.local/2025/07/30/bison-featured-image/?enable_plugins=dominant-color-images" \
--number=250 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
Moreover, there is no difference in the Lighthouse performance score which is already maxed out at 100 (but again, this doesn’t mean perfection). Nevertheless, just because there is no improvement on the raw performance metric, this doesn’t mean there isn’t value in doing it. User-perceived performance is also important, as long as it doesn’t negatively impact LCP (which should hopefully not conflict). We’ll revisit this later with View Transitions.
Modern Image Formats
Modern image formats, like WebP and AVIF, are able to compress much higher compared to older formats like JPEG and PNG. For example, an image compressed with AVIF could be 50% smaller than a JPEG with similar visual quality. It stands to reason that if an image is smaller, then it will take less time to download, and the LCP metric will be improved since the image can render sooner. This also addresses a common audit you encounter in Lighthouse to serve images in next-gen formats:

Note that the audit here estimates that the image in a modern image format would be 54% smaller for this image. (Note also the shameless plug for Performance Lab thanks to the Stack Pack for WordPress.)
The Modern Image Formats plugin (originally called “WebP Uploads”) addresses this audit’s complaint by converting uploaded images into AVIF or WebP, depending on which is available on your server. With the plugin active, the original Bison 🦬 image uploaded as a JPEG is compressed from 356 KB down to 292 KB in AVIF format. This is ~18% smaller, not the hoped-for ~50% reduction in file size. Nevertheless, will this yield a 18% improvement in LCP? Here are the results of testing the same page as when testing Image Placeholders above, a post where the featured image is the LCP element:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 438.8 | 426.8 | -12.1 | -2.7% |
LCP | 613.8 | 599.2 | -14.6 | -2.4% |
TTFB | 47.8 | 49.2 | +1.4 | +2.8% |
LCP-TTFB | 565.1 | 550.6 | -14.5 | -2.6% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/2025/07/30/bison-featured-image/?enable_plugins=none" \
--url="https://wcus-perf-talk-demo.local/2025/07/30/bison-featured-image/?enable_plugins=webp-uploads" \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
So while the image file size was reduced ~20%, the LCP improvement here was only ~2%.
Brendan Kenny’s article on Common Misconceptions About How to Optimize LCP shows that among the LCP sub-parts, the TTFB and “image load delay” contribute much more to the overall time compared with actually downloading the image resource. (Also described in Web Almanac.) Remkus de Vries has likewise emphasized that we should Stop Obsessing Over Image Optimization. We absolutely shouldn’t be serving 10 MB images to visitors, but there are diminishing returns for optimizing LCP with each percentage reduction in an image’s file size. There are far more impactful ways to improve LCP than to use the most optimal image compression.
Thanks to Adam Silverstein for championing support for modern image formats both in this plugin and in core!
Enhanced Responsive Images
The Enhanced Responsive Images plugin was originally developed as a way to automatically add sizes=auto
for images with loading=lazy
. This new part of the HTML spec lets the browser compute the responsive sizes because lazy-loaded images are loaded after the page has been laid out. This enhancement landed in WordPress 6.7. Since then, the scope of the plugin has changed to improve the calculation of the responsive sizes
attribute for images which are not lazy-loaded.
By default, WordPress uses the same formula for constructing the default sizes
attribute for all images. For example, if an image is 1024 pixels wide, then the sizes
attribute is set to:
(max-width: 1024px) 100vw, 1024px
Code language: plaintext (plaintext)
This is problematic, however, because if the image takes up half the width of the screen, then the browser will select the image URL from the srcset
attribute for the size corresponding to the width of the viewport, not the width of the actual IMG
element. This is often fine on mobile when images are more often taking up the full page width, but on desktop viewports it means a much larger image will be downloaded than is appropriate for the container size. For example, consider these images in a Columns block (sourced from Wikipedia, as linked):
These images were all resized to be 1024 pixels wide, and so using the default WordPress scheme, they all have the same sizes
attribute (as shown above), in spite of the fact that the first IMG
element is twice the width of the second and third, and 1024px itself is about double the entire 645px width of the root Columns block on desktop.
In Lighthouse, the properly size images audit correctly identifies these images as having inaccurate sizes
:

On my test page, this Columns block is at the beginning of the content, so none of the IMG
tags are lazy-loaded and likewise none are eligible for auto-sizes. This is where the enhanced Enhanced Responsive Sizes plugin comes in. Now that auto-sizes was merged into core, the plugin’s scope has changed to improve the accuracy of the sizes
attribute by using the structured layout information available in block themes (which is not available in classic themes). With this plugin active, the width in the sizes
attribute for the IMG
in the first column reduces from 1024px down to 429px:
(max-width: 429px) 100vw, 429px
Code language: plaintext (plaintext)
And the two smaller IMG
tags in the second narrower column get reduced from 1024px down to 134px:
(max-width: 134px) 100vw, 134px
Code language: plaintext (plaintext)
Here is the performance impact when benchmarking the change:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 436.1 | 438.6 | +2.5 | +0.6% |
LCP | 915.3 | 690.9 | -224.4 | -24.5% |
TTFB | 50.8 | 50.6 | -0.2 | -0.3% |
LCP-TTFB | 865.6 | 638.3 | -227.3 | -26.3% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/2025/07/31/bison-two-columns/?enable_plugins=none" \
--url="https://wcus-perf-talk-demo.local/2025/07/31/bison-two-columns/?enable_plugins=auto-sizes" \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
This has a dramatic ~25% reduction in LCP!
This also has an improvement in the Lighthouse score for this example page, whereas I did not find an improvement when testing the previous plugins.


The optimization also greatly improved the properly size images audit, before and after:


Note that this audit is unlikely to ever pass completely unless you generate many more intermediate image sizes to better fit all possible dimensions for your responsive images. This is something that an image CDN could do for you, however.
The effect of the more accurate sizes
can be is evident in which intermediate image size files get downloaded:
Before | After | Reduction | |
---|---|---|---|
![]() | 2048×1336 | 1024×668 | -75% |
![]() | 2048×1483 | 300×217 | -98% |
![]() | 2048×1413 | 300×207 | -98% |
Total: | 8,667,136px | 811,232px | -91% |
In this test of images in a Columns block, what follows is the impact of Modern Image Formats with AVIF versus Enhanced Responsive Images with more accurate sizes
, and then with them both active together:
Plugins | Transferred | Reduction |
---|---|---|
None | 1,595 kB | — |
Modern Image Formats with AVIF | 1,137 kB | 29% |
Enhanced Responsive Images | 206 kB | 87% |
Both | 175 kB | 89% |
As is evident, the use of a more accurate sizes
attribute has three times the reduction in bytes compared with using the AVIF image format (87% vs 29%)! Adding AVIF on top of the better sizes
only yields an additional 2% reduction in transferred bytes in this example. It’s no wonder why Enhanced Responsive Images has a greater impact on LCP compared with Modern Image Formats!
Props to Mukesh Panchal and Joe McGill for their work on this! Joe also first proposed the original sizes
attribute.
Image Prioritizer
The last Performance Lab feature plugin which focuses on images is Image Prioritizer. As indicated by the name, the plugin optimizes image loading prioritization. For example, it boosts the priority of the detected LCP image with fetchpriority=high
while also deprioritizing the loading of images outside the viewport with lazy-loading. This plugin depends on the Optimization Detective plugin as its framework for the optimizations it applies. I gave a talk at WordCamp Asia 2025 all about this plugin:
In that talk, I cover Image Prioritizer in depth; the plugin description also has the full list of optimizations. But I’ll highlight here a couple of the most impactful optimizations which improve the LCP metric for images.
Responsive Image Prioritization
Take for example this gallery of three images (again, from Wikipedia as linked):



This gallery is configured without the “Crop images to fit” setting enabled. On desktop, the second image is the largest image of the three, and so it is the LCP element. However, on mobile it’s actually the third image which is the largest (and the LCP element) since it appears on a row by itself:


Nevertheless, WordPress core adds fetchpriority=high
to the first IMG
, of the bison and calf, even though it is never the LCP element. WordPress adds the fetchpriority
attribute to the first sufficiently-large image it finds on the page, making a best guess as to which is the LCP element. But even when core does add the attribute to the right image on a desktop viewport, it could be wrong for mobile, and vice versa. In my research, when WordPress core correctly adds the fetchpriority
attribute to the LCP IMG
element on desktop or mobile, I found that 37% of those pages have a different IMG
which is the LCP element for the other viewport. This means it’s only safe to use the fetchpriority
attribute on IMG
tags when they are the LCP element on both desktop and mobile (and tablet too). But WordPress doesn’t know how the page is laid out (although this is starting to change in the case of block themes, as with Enhanced Responsive Images above). This is where Optimization Detective comes in.
The Optimization Detective plugin provides a framework to capture measurements from site visitors about what elements are displayed on a page across a variety of device form factors and responsive breakpoints (e.g. desktop, tablet, and mobile). These measurements are stored in “URL Metrics” (a custom post type) which can then be used by extensions, like Image Prioritizer, to apply more accurate optimizations. In this case, Image Prioritizer:
- Removes
fetchpriority=high
from the firstIMG
in the Gallery. - Adds responsive preload
LINK
tags for the actual LCP element based on media queries.
For example, the following LINK
tags are added to the page:
<link
rel="preload" as="image" fetchpriority="high"
href=".../bison-2.jpg"
imagesrcset="..." imagesizes="..."
media="screen and (width <= 480px)"
>
<link
rel="preload" as="image" fetchpriority="high"
href=".../bison-3.jpg"
imagesrcset="..." imagesizes="..."
media="screen and (782px < width)"
>
Code language: HTML, XML (xml)
Note how the first LINK
preloads the second bison image on mobile, but the second LINK
preloads the third bison image on desktop. Here is the performance impact for these changes on mobile:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 441.9 | 449.5 | +7.7 | +1.7% |
LCP | 984.1 | 713.2 | -270.9 | -27.5% |
TTFB | 49.4 | 53.5 | +4.1 | +8.3% |
LCP-TTFB | 935.1 | 659.5 | -275.6 | -29.5% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/2025/08/04/bison-gallery/?disable_all_plugins" \
--url="https://wcus-perf-talk-demo.local/2025/08/04/bison-gallery/?enable_plugins=image-prioritizer" \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
This is the biggest LCP improvement I’ve yet shown, with a 27.5% reduction compared with the 24.5% improvement in Enhanced Responsive Images. This shows up as an improvement in the Lighthouse performance score, increasing from 95 to 99:


But what becomes truly impressive are the results on desktop:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 434.9 | 436.0 | +1.2 | +0.3% |
LCP | 1020.1 | 503.2 | -517.0 | -50.7% |
TTFB | 49.9 | 52.4 | +2.6 | +5.1% |
LCP-TTFB | 969.4 | 451.3 | -518.2 | -53.5% |
The LCP improvement here on desktop is almost double the improvement on mobile, at an over 50% reduction in LCP! In other words, the LCP metric is cut in half! This shows impressively in the Lighthouse performance score increasing from 93 to 100:


Background Image Prioritization
Related to there being different LCP IMG
elements on desktop versus mobile is that the LCP element’s image may not be an IMG
at all, but rather a DIV
(or some other element) with a CSS background-image
. This is a very common way that imagery is added in page builders. Background images are also present in WordPress core, such as in some classic themes’ header images; background images are also on any WordPress site using the Cover block when using a fixed background or when adding a background image to a Group block. The prevalence of non-IMG
LCP images is captured in this data presented in Web Almanac 2022, showing that the DIV
(presumably with a background image) is the LCP element ~26% of the time compared with an IMG
at 42% of the time:

The problem with the background-image
style is that it is CSS: there is no way for core to attach a fetchpriority=high
HTML attribute as can be done for LCP IMG
candidates. Take the following page for example, where there is a parallax Cover block at the beginning of the content, followed by some paragraphs of text, and finally a Gallery block with five images in it. The black rectangle denotes the desktop viewport:

Cover image courtesy Gintare K. on Pexels. Other previously-unused images courtesy Wikipedia: 1, 2, 3.
The DIV
in the Cover block with the CSS background-image
is the LCP element. Nevertheless, WordPress core is adding fetchpriority=high
to the first IMG
in the Gallery block because it is the first sufficiently large image, just in terms of its width
and height
attributes. Additionally, WordPress core omits loading=lazy
from the first three content images (the first three images in the Gallery), but they are not even visible on either the desktop or mobile viewports. The effect here is that the first three images of the Gallery are all loaded first before the all-important background image for the Cover block. Image Prioritizer fixes this by:
- Removing
fetchpriority=high
from the firstIMG
in the Gallery, since it is not the LCP element. - Adding
loading=lazy
to the first threeIMG
tags in the Gallery, since none of them are visible in any initial viewport. - Adding a preload
LINK
for the CSSbackground-image
so that it is properly prioritized.
The preload LINK
looks like the following:
<link
rel="preload"
as="image"
fetchpriority="high"
href=".../bison.jpg"
media="screen"
>
Code language: HTML, XML (xml)
Unlike with the responsive image prioritization, the Cover block here is the LCP element for both desktop and mobile, so here there is only one LINK
and the media
attribute doesn’t need to add any viewport constraints.
Here is the performance impact:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 436.5 | 433.9 | -2.7 | -0.6% |
LCP | 1042.4 | 579.8 | -462.7 | -44.4% |
TTFB | 49.0 | 53.1 | +4.1 | +8.4% |
LCP-TTFB | 994.6 | 526.9 | -467.7 | -47.0% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url=https://wcus-perf-talk-demo.local/2025/08/04/cover-block/?disable_all_plugins \
--url=https://wcus-perf-talk-demo.local/2025/08/04/cover-block/?enable_plugins=image-prioritizer \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
This is the second-best improvement to LCP I’ve shown here in analyzing these plugins. The Lighthouse performance score is also improved from 92 to 99:


Surely nothing can improve LCP more than what was achieved here with the Image Prioritizer plugin, right? Read on.
Speculative Loading
The Speculative Loading plugin is the first discussed here not specifically focused on improving LCP for images, although they do benefit. This was a feature plugin actually to bring the Speculation Rules API to WordPress core, which was merged in 6.8. This API allows pages to either:
- Prefetch a link, reducing TTFB to zero.
- Prerender a link, potentially reducing LCP to zero.
As such, Speculative Loading is somewhat cheating at performance because you can’t get any faster at loading something than to have it already loaded.
The degree by which TTFB and LCP are improved is largely dependent on the “eagerness” of the speculation. There are three main eagerness values for when speculation starts:
- Conservative: when you pointer-down on a link.
- Moderate: when you hover over a link (or soon on mobile when a link is in the viewport).
- Eager: right away without any user interaction.
For the initial core merge, the default cautious configuration was to use prefetch with conservative eagerness. Conservative eagerness was to avoid unused speculations which can overly tax under-powered servers, and prefetching was to avoid potential compatibility issues with prerendering, such as with analytics or ads.
Here’s the impact that the various configurations of Speculative Loading have on LCP:
No Speculation
2.17 s
Conservative Prefetch
2.12 s
(default as of WP 6.8)
Moderate Prefetch
1.04 s
Moderate Prerender
0.04 s
Navigation with prerendering results in a practically instantaneous page load with a near zero LCP! In all these cases the LCP is still considered “good” at being less than 2.5 seconds, but just because something is good doesn’t mean it can’t be better!
Note that the test page here adds a 1 second TTFB via sleep(1)
. This reflects a fairly typical server response time considering that only a quarter of WordPress sites have a good TTFB passing rate, which is 800 ms and faster.
Props to Felix Arntz for spearheading this feature and landing it in core.
View Transitions
As described in the previous example, page navigations with Speculative Loading can be nearly instant with prerendering. This is great, but it’s almost too good. The navigation can feel so instant as to be abrupt. There can also be a white flicker between the page loads. To help in part with having too much of a good thing, the newest plugin to be featured in Performance Lab is View Transitions. There is a new web platform feature for cross-document view transitions for multi-page applications, and this plugin brings these smooth page navigation animations to WordPress. With Speculative Loading and View Transitions, navigating around a regular multi-page WordPress site can feel as fluid as a single-page app (and without all the implementation complexity).
Take a look at the impact on the user experience when navigating between the homepage and a blog post:
Note that these view transitions apply not only when navigating via links, but they also apply when navigating with the back/forward buttons in the browser:
Nevertheless, as nice as these cross-document view transitions are, do note that there is no LCP improvement to using them. As referenced previously with Image Placeholders, the View Transitions plugin provides a non-performance user experience improvement. So don’t expect to find any difference in your Lighthouse scores or LCP passing rates with this plugin.
Props again to Felix Arntz for spearheading this feature plugin.
No-cache BFCache
Originally, the No-cache BFCache plugin was part of the “beyond” part of my talk because it wasn’t among the plugins featured by Performance Lab. However, this is no longer the case since v4.0.0. In the previous section about Speculative Loading, I showed how prerendering enabled near instant page loads with practically zero LCP. But there is a much older browser technology for instantaneous page navigations: the back-forward cache (bfcache). This was also depicted above in the back/forward navigation videos with view transitions.
I wrote up a blog post already all about bfcache and this plugin:
To recap, webpages are generally not eligible for bfcache when they are served with Cache-Control: no-store
. This header is sent when a user is logged-in and often on e-commerce sites for the shopping cart, checkout, and account pages. While it importantly prevents such pages from being cached by proxies, it also prevents the browser from storing pages in bfcache. This plugin removes the no-store
directive. In its place, it ensures that the private
directive is sent to prevent proxies from caching the response; also, to ensure preserve privacy after logging out, it includes logic to invalidate pages from the bfcache so they cannot be re-accessed.
What follows is an example of a site running Twenty Twenty-Five with the BuddyPress plugin and Slow 4G network emulation. After entering an activity status update, I navigate from the Personal tab to the Mentions and Favorites tabs. Then I use the back button to go back to the Personal tab. Without bfcache, navigating back from the Favorites tab to the Personal tab is very slow since (1) the browser has to re-fetch the HTML from the server, and (2) the DOM has to be completely reconstructed. Without bfcache, there is also the unfortunate result that the drafted status update is lost, since the form field was re-constructed with JavaScript. In contrast, when bfcache is enabled, navigating to the previous tabs is instant, and the DOM is preserved with each navigation, resulting in the drafted status update being kept intact:
Without bfcache, the back navigation has an LCP of 1.41 seconds whereas with bfcache the LCP is 0.02 seconds: nearly instantaneous.
There are other reasons why pages may be ineligible for bfcache than the no-store
directive, but it is one of the most common causes. It’s very important to try to preserve bfcache eligibility because back/forward navigations are very common on the web:
Chrome usage data shows that 1 in 10 navigations on desktop and 1 in 5 on mobile are either back or forward.
Script Module Deprioritization
Moving on from instant page loads with Speculative Loading and bfcache, another way to shave off milliseconds on the LCP metric is to reduce network contention for loading the LCP element resource (e.g. an image). Consider a template with an Image block and a Navigation block, where the Image block has a lightbox and the Navigation block expands on mobile. These blocks use the Interactivity API which involves adding script modules to the page with the necessary logic. As noted previously, one of the key design principles of the Interactivity API is server-side rendering. This means that by design the Navigation block and the Image block do not need their script modules in the critical rendering path.
It turns out that these script modules are currently loaded with high priority because the browser doesn’t know they aren’t critical. So they compete with the loading of critical resources, like the LCP image, even though script modules aren’t render blocking.
I’ve written a separate post all about this problem and the solution:
To summarize, there are two ways to prevent script modules from delaying the loading of critical resources:
- Add
fetchpriority=low
to theSCRIPT
module tags and themodulepreload
LINK
. - Move the
SCRIPT
tags to the end of theBODY
(the footer).
Here are the results of these optimizations on an emulated broadband connection with an IMG
as the LCP element:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 137.0 | 137.2 | +0.2 | +0.1% |
LCP | 406.0 | 368.8 | -37.2 | -9.2% |
TTFB | 33.7 | 33.6 | -0.1 | -0.1% |
LCP-TTFB | 371.7 | 336.0 | -35.7 | -9.6% |
This is a healthy LCP improvement, more impactful than using the Modern Image Formats with the AVIF format in my testing above. There are two plugins available on GitHub which implement these optimizations while waiting for them to be available in core:
Minified CSS Inlining
The final optimization I’m analyzing is the impact of eliminating render-blocking external stylesheets. With JavaScript, adding defer
to a SCRIPT
is an easy way to prevent them from blocking rendering (assuming they can be deferred). However, this is not so easy to do with external stylesheets. CSS is always render-blocking because otherwise there is a flash of unstyled content (FOUC). The web platform does not (currently) provide an official way to opt in to async CSS. Instead, the best way to handle this is to inline the CSS in STYLE
tags (at least for the critical CSS).
On a vanilla WordPress install when loading the Sample Page, where the LCP element is text, there are two render-blocking stylesheets:
- The Navigation block’s
style.min.css
- The Twenty Twenty-Five theme’s
style.css

Despite these render-blocking stylesheets, Lighthouse is giving the page a 100 performance score. But as I’ve said before, just because you have a 100 score in Lighthouse, this doesn’t mean you can do more. Even with a perfect Lighthouse score, there is actually an audit that is pointing out the performance problem: Eliminate render-blocking resources.

It’s strange that this audit has an overall estimated savings of zero milliseconds, but for the theme’s stylesheet it shows an estimated savings of 150 milliseconds.
To inline these two stylesheets to prevent them from being render-blocking, what is needed is to:
- Opt in to inline the (minified) theme’s
style.css
. - Increase the
styles_inline_size_limit
.
To inline Twenty Twenty-Five’s stylesheet, all that is required is to add the path
data for where the registered style is located on the filesystem. This can be done as simply as follows:
add_action(
'wp_enqueue_scripts',
function (): void {
wp_style_add_data(
'twentytwentyfive-style',
'path',
get_parent_theme_file_path( 'style.css' )
);
},
20
);
Code language: PHP (php)
However, since the stylesheet is not yet minified (cf. #63012), you can hack in runtime minification using a plugin like Twenty Twenty-Five Stylesheet Inlining. This plugin is currently just in a Gist since I hope this will land soon in core for 6.9 via #63007.
To increase the limit for inline CSS, all that is needed is a simple filter. The default limit is 20 KB which seems low considering the inline CSS limit for an AMP page is 75 KB. To increase the limit to 30 KB which allows enough room for the Navigation block’s relatively stylesheet to be inlined, you can use this PHP code:
add_filter(
'styles_inline_size_limit',
function (): int {
return 30000;
}
);
Code language: PHP (php)
Plugin used for benchmarking below
<?php
/**
* Plugin Name: Increase Styles Inline Size Limit (styles_inline_size_limit)
* Author: Weston Ruter
* Update URI: false
*/
add_filter(
'styles_inline_size_limit',
static function (): int {
$limit = -1;
if ( isset( $_GET['styles_inline_size_limit'] ) ) {
$limit = (int) $_GET['styles_inline_size_limit'];
}
if ( $limit < 0 ) {
$limit = 75000;
}
return $limit;
}
);
Code language: PHP (php)
Increasing this limit in core is being tracked in #63018. We still need to determine the optimal threshold for inlining, weighing against the benefits of serving stylesheets from the browser cache for subsequent page navigations.
As for the performance impact of inlining these stylesheets, here are the results for the loading Sample Page on an emulated Fast 4G connection:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 409.1 | 228.4 | -180.7 | -44.2% |
LCP | 510.0 | 325.4 | -184.6 | -36.2% |
TTFB | 43.3 | 43.8 | +0.6 | +1.3% |
LCP-TTFB | 466.5 | 281.2 | -185.4 | -39.7% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/sample-page/?enable_plugins=twentytwentyfive-stylesheet-inlining" \
--url="https://wcus-perf-talk-demo.local/sample-page/?enable_plugins=twentytwentyfive-stylesheet-inlining,increase-styles-inline-size-limit.php&styles_inline_size_limit=30000" \
--number=50 \
--network-conditions="Fast 4G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
This decreases the LCP by over a third!
And here are the results when emulating a Slow 3G connection:
Metric | Before | After | Diff (ms) | Diff (%) |
---|---|---|---|---|
FCP | 4206.5 | 2276.0 | -1930.5 | -45.9% |
LCP | 4308.3 | 2384.6 | -1923.7 | -44.7% |
TTFB | 42.6 | 45.5 | +2.85 | +6.7% |
LCP-TTFB | 4265.9 | 2339.7 | -1926.3 | -45.2% |
Benchmark command
npm run research -- benchmark-web-vitals \
--url="https://wcus-perf-talk-demo.local/sample-page/?enable_plugins=twentytwentyfive-stylesheet-inlining" \
--url="https://wcus-perf-talk-demo.local/sample-page/?enable_plugins=twentytwentyfive-stylesheet-inlining,increase-styles-inline-size-limit.php&styles_inline_size_limit=30000" \
--number=50 \
--network-conditions="Slow 3G" \
--emulate-device="Moto G4" \
--diff \
--output=md
Code language: JavaScript (javascript)
A 44.4% reduction in LCP is on par with the largest improvements achieved by the Image Prioritizer plugin in my evaluations here. This means that on a Slow 3G connection, the LCP goes from poor at 4.31 seconds to good at 2.38 seconds.
What’s Next
My hope is that several of these improvements will land later this year in WordPress core. Some of them are tracked in the Roadmap to 6.9:
Planned performance improvements include improving Data Views performance by supporting partial entity fetching and smart field resolution, adding the ability to handle “fetchpriority” to ES Modules and Import Maps, standardizing output buffering so developers can hook into a unified filter and manipulate the entire rendered HTML after it’s generated but before it’s sent to the browser (e.g. for page caches and performance optimizations), implementing instant page navigations from browser history via bfcache even when pages are flagged with “nocache” such as when users are logged in, and stylesheet improvements around minification and inlining.
You can get involved with the Core Performance Team to help make this happen!
Where I’ve shared this, if you want to discuss or boost:
Leave a Reply