Skip to content

Conversation

@westonruter
Copy link
Collaborator

@westonruter westonruter commented Apr 1, 2025

I discovered that benchmark-web-vitals was reusing the same browser instance across all requests. This meant that assets cached for the initial request would then be available for subsequent requests. When testing the performance impact of preloading an image, for example, this would mean that if you first tested the URL with the image preloaded and then tested the page without the preloading, you'd see barely any an improvement (if any). But if you switched the order around, then you'd see a big improvement if the URL with the image preloading happened after the request without the preloading. So this PR makes sure that a fresh browser instance is used for every request to prevent one request from impacting the performance of another request. As an additional safeguard, it also explicitly disables the cache for the launched browser.

Test Setup

I used LocalWP for my test and I added an .htaccess file that enabled far-future expires for image files. Since typically local development environments are configured to serve assets with Cache-Control: no-cache, the issue may not have apparent when compared with benchmarking a production environment. In #178 I'm benchmarking production URLs with a single request to a URL with Optimization Detective disabled followed by a single request to Optimization enabled, and as can be seen below, the effect of the browser cache is very apparent.

I have configured a post to contain six images, five of which are IMG elements in columns (where the first gets fetchpriority=high by WordPress core) and sixth one which is a CSS background-image of a Group block which is the actual LCP element:

image

Block Markup
<!-- wp:columns {"isStackedOnMobile":false} -->
<div class="wp-block-columns is-not-stacked-on-mobile"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:image {"id":9,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="http://localhost:10079/wp-content/uploads/2025/04/bison2-1024x673.jpg" alt="" class="wp-image-9"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:image {"id":10,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="http://localhost:10079/wp-content/uploads/2025/04/bison3-1024x683.jpg" alt="" class="wp-image-10"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:image {"id":11,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="http://localhost:10079/wp-content/uploads/2025/04/bison4-1024x768.jpg" alt="" class="wp-image-11"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:image {"id":12,"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full"><img src="http://localhost:10079/wp-content/uploads/2025/04/bison5.jpg" alt="" class="wp-image-12"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:image {"id":13,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="http://localhost:10079/wp-content/uploads/2025/04/bison6-1024x707.jpg" alt="" class="wp-image-13"/></figure>
<!-- /wp:image --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:group {"style":{"background":{"backgroundImage":{"url":"http://localhost:10079/wp-content/uploads/2025/04/bison1-scaled.jpg","id":8,"source":"file","title":"bison1"},"backgroundSize":"cover"}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Bison</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph --></div>
<!-- /wp:group -->

I have the Twenty Twenty-Five theme active as well as the following plugins:

image

Note I've added plugins that inline all stylesheets so that the differences in image prioritization are more pronounced.

I have two files containing the URLs for this test post with Optimization Detective disabled and enabled, and then enabled and disabled:

od-disabled-then-od-enabled.txt:

http://localhost:10079/od-test-page/?optimization_detective_disabled=1
http://localhost:10079/od-test-page/?optimization_detective_enabled=1

od-enabled-then-od-disabled.txt:

http://localhost:10079/od-test-page/?optimization_detective_disabled=1
http://localhost:10079/od-test-page/?optimization_detective_enabled=1
Diff of HTML with Optimization Detective disabled and enabled
--- /tmp/disabled.html	2025-03-31 23:28:23.283309786 -0700
+++ /tmp/enabled.html	2025-03-31 23:28:48.867310575 -0700
@@ -3232,6 +3232,14 @@
           format("woff2");
       }
     </style>
+    <link
+      data-od-added-tag
+      rel="preload"
+      fetchpriority="high"
+      as="image"
+      href="http://localhost:10079/wp-content/uploads/2025/04/bison1-scaled.jpg"
+      media="screen"
+    />
   </head>
 
   <body
@@ -3406,7 +3414,9 @@
               >
                 <figure class="wp-block-image size-large">
                   <img
-                    fetchpriority="high"
+                    data-od-removed-fetchpriority="high"
+                    data-od-replaced-sizes="(max-width: 1024px) 100vw, 1024px"
+                    data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::DIV]/*[1][self::DIV]/*[1][self::FIGURE]/*[1][self::IMG]"
                     decoding="async"
                     width="1024"
                     height="673"
@@ -3420,7 +3430,7 @@
                       http://localhost:10079/wp-content/uploads/2025/04/bison2-1536x1010.jpg 1536w,
                       http://localhost:10079/wp-content/uploads/2025/04/bison2-2048x1347.jpg 2048w
                     "
-                    sizes="(max-width: 1024px) 100vw, 1024px"
+                    sizes="(width &lt;= 480px) 42px, (480px &lt; width &lt;= 600px) 75px, (600px &lt; width &lt;= 782px) 99px, (782px &lt; width) 90px"
                   />
                 </figure>
               </div>
@@ -3430,6 +3440,8 @@
               >
                 <figure class="wp-block-image size-large">
                   <img
+                    data-od-replaced-sizes="(max-width: 1024px) 100vw, 1024px"
+                    data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::DIV]/*[2][self::DIV]/*[1][self::FIGURE]/*[1][self::IMG]"
                     decoding="async"
                     width="1024"
                     height="683"
@@ -3443,7 +3455,7 @@
                       http://localhost:10079/wp-content/uploads/2025/04/bison3-1536x1025.jpg 1536w,
                       http://localhost:10079/wp-content/uploads/2025/04/bison3-2048x1367.jpg 2048w
                     "
-                    sizes="(max-width: 1024px) 100vw, 1024px"
+                    sizes="(width &lt;= 480px) 42px, (480px &lt; width &lt;= 600px) 75px, (600px &lt; width &lt;= 782px) 99px, (782px &lt; width) 90px"
                   />
                 </figure>
               </div>
@@ -3453,6 +3465,8 @@
               >
                 <figure class="wp-block-image size-large">
                   <img
+                    data-od-replaced-sizes="(max-width: 1024px) 100vw, 1024px"
+                    data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::FIGURE]/*[1][self::IMG]"
                     decoding="async"
                     width="1024"
                     height="768"
@@ -3466,7 +3480,7 @@
                       http://localhost:10079/wp-content/uploads/2025/04/bison4-1536x1152.jpg 1536w,
                       http://localhost:10079/wp-content/uploads/2025/04/bison4-2048x1536.jpg 2048w
                     "
-                    sizes="(max-width: 1024px) 100vw, 1024px"
+                    sizes="(width &lt;= 480px) 42px, (480px &lt; width &lt;= 600px) 75px, (600px &lt; width &lt;= 782px) 99px, (782px &lt; width) 90px"
                   />
                 </figure>
               </div>
@@ -3476,7 +3490,9 @@
               >
                 <figure class="wp-block-image size-full">
                   <img
-                    loading="lazy"
+                    data-od-removed-loading="lazy"
+                    data-od-replaced-sizes="auto, (max-width: 1024px) 100vw, 1024px"
+                    data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::DIV]/*[4][self::DIV]/*[1][self::FIGURE]/*[1][self::IMG]"
                     decoding="async"
                     width="1024"
                     height="768"
@@ -3488,7 +3504,7 @@
                       http://localhost:10079/wp-content/uploads/2025/04/bison5-300x225.jpg  300w,
                       http://localhost:10079/wp-content/uploads/2025/04/bison5-768x576.jpg  768w
                     "
-                    sizes="auto, (max-width: 1024px) 100vw, 1024px"
+                    sizes="(width &lt;= 480px) 42px, (480px &lt; width &lt;= 600px) 75px, (600px &lt; width &lt;= 782px) 99px, (782px &lt; width) 90px"
                   />
                 </figure>
               </div>
@@ -3498,7 +3514,9 @@
               >
                 <figure class="wp-block-image size-large">
                   <img
-                    loading="lazy"
+                    data-od-removed-loading="lazy"
+                    data-od-replaced-sizes="auto, (max-width: 1024px) 100vw, 1024px"
+                    data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[1][self::DIV]/*[5][self::DIV]/*[1][self::FIGURE]/*[1][self::IMG]"
                     decoding="async"
                     width="1024"
                     height="707"
@@ -3512,13 +3530,14 @@
                       http://localhost:10079/wp-content/uploads/2025/04/bison6-1536x1060.jpg 1536w,
                       http://localhost:10079/wp-content/uploads/2025/04/bison6-2048x1413.jpg 2048w
                     "
-                    sizes="auto, (max-width: 1024px) 100vw, 1024px"
+                    sizes="(width &lt;= 480px) 42px, (480px &lt; width &lt;= 600px) 75px, (600px &lt; width &lt;= 782px) 99px, (782px &lt; width) 90px"
                   />
                 </figure>
               </div>
             </div>
 
             <div
+              data-od-xpath="/HTML/BODY/DIV[@class=&#039;wp-site-blocks&#039;]/*[2][self::MAIN]/*[1][self::DIV]/*[3][self::DIV]/*[2][self::DIV]"
               style="
                 background-image: url(&#039;http://localhost:10079/wp-content/uploads/2025/04/bison1-scaled.jpg&#039;);
                 background-size: cover;
@@ -3606,7 +3625,7 @@
                   ><a
                     rel="nofollow"
                     id="cancel-comment-reply-link"
-                    href="/od-test-page/?optimization_detective_disabled=1#respond"
+                    href="/od-test-page/?optimization_detective_enabled=1#respond"
                     style="display: none"
                     >Cancel reply</a
                   ></small
@@ -4009,5 +4028,50 @@
         sibling.parentElement.insertBefore(skipLink, sibling);
       })();
     </script>
+    <script type="module">
+      import detect from "http:\/\/localhost:10079\/wp-content\/plugins\/optimization-detective\/detect.min.js?ver=1.0.0-beta4";
+      detect({
+        minViewportAspectRatio: 0,
+        maxViewportAspectRatio: 9.2233720368547758e18,
+        isDebug: false,
+        extensionModuleUrls: [
+          "http:\/\/localhost:10079\/wp-content\/plugins\/image-prioritizer\/detect.min.js?ver=1.0.0-beta2",
+        ],
+        restApiEndpoint:
+          "http:\/\/localhost:10079\/wp-json\/optimization-detective\/v1\/url-metrics:store",
+        currentETag: "2d6be0f1c8afefed0c3d6b0cfe55ad71",
+        currentUrl:
+          "http:\/\/localhost:10079\/od-test-page\/?optimization_detective_enabled=1",
+        urlMetricSlug: "1ac13259841e381c8b92b1ff1bd604c8",
+        cachePurgePostId: 6,
+        urlMetricHMAC: "e2a221a15d5664dc13261bac38b94d66",
+        urlMetricGroupStatuses: [
+          {
+            minimumViewportWidth: 0,
+            maximumViewportWidth: 480,
+            complete: false,
+          },
+          {
+            minimumViewportWidth: 480,
+            maximumViewportWidth: 600,
+            complete: false,
+          },
+          {
+            minimumViewportWidth: 600,
+            maximumViewportWidth: 782,
+            complete: false,
+          },
+          {
+            minimumViewportWidth: 782,
+            maximumViewportWidth: null,
+            complete: false,
+          },
+        ],
+        storageLockTTL: 0,
+        freshnessTTL: 0,
+        webVitalsLibrarySrc:
+          "http:\/\/localhost:10079\/wp-content\/plugins\/optimization-detective\/build\/web-vitals.js?ver=4.2.4",
+      });
+    </script>
   </body>
 </html>

In the following tests I've added Fast 4G network emulation because without it, requests to load images from localhost happen extremely fast and any effect of image prioritization optimizations may be not apparent.

Before

With 10 iterations

for file in "od-disabled-then-od-enabled.txt" "od-enabled-then-od-disabled.txt"; do
  npm run research -- benchmark-web-vitals -f "$file" -n 10 -o csv -c "Fast 4G"; 
done | tee output.txt

Output:

> research
> ./cli/run.mjs benchmark-web-vitals -f od-disabled-then-od-enabled.txt -n 10 -o csv -c Fast 4G

URL,http://localhost:10079/od-test-page/?optimization_detective_disabled=1,http://localhost:10079/od-test-page/?optimization_detective_enabled=1
Success Rate,100%,100%
FCP (median),286,270
LCP (median),352,338
TTFB (median),169.35,157.3
LCP-TTFB (median),201.15,172.55


> research
> ./cli/run.mjs benchmark-web-vitals -f od-enabled-then-od-disabled.txt -n 10 -o csv -c Fast 4G

URL,http://localhost:10079/od-test-page/?optimization_detective_enabled=1,http://localhost:10079/od-test-page/?optimization_detective_disabled=1
Success Rate,100%,100%
FCP (median),264,280
LCP (median),346,340
TTFB (median),153.15,136.3
LCP-TTFB (median),194.25,198.7

Notice how the LCP-TTFB is very small, even though it's on "Fast 4G". Notice also how in the first call to benchmark-web-vitals when the OD disabled URL goes before the OD enabled URL, the LCP-TTFB is much greater that when the OD enabled URL is benchmarked before the OD disabled URL. Often, whichever URL is requested first will be the one that appears worse because it is the one that is hit with a cold browser cache. This is made more stark when reducing the iterations.

With 1 iteration over 10 repetitions

for i in {1..10}; do 
  for file in "od-disabled-then-od-enabled.txt" "od-enabled-then-od-disabled.txt"; do 
    npm run research -- benchmark-web-vitals -f "$file" -n 1 -o csv -c "Fast 4G"; 
  done | grep 'LCP-TTFB'; 
done | tee output.txt

Output, where the cells in the first column alternate between OD Disabled and OD Enabled:

LCP-TTFB (median),1996.6,182.3
LCP-TTFB (median),1430.3,194
LCP-TTFB (median),2059.9,191.3
LCP-TTFB (median),1450.8,177.5
LCP-TTFB (median),2152.6,186.7
LCP-TTFB (median),1450.1,186.9
LCP-TTFB (median),1994.6,217.3
LCP-TTFB (median),1441.8,179.4
LCP-TTFB (median),2038.1,180.7
LCP-TTFB (median),1448,188.2
LCP-TTFB (median),1960,187.5
LCP-TTFB (median),1430.6,187.5
LCP-TTFB (median),2026.7,189.7
LCP-TTFB (median),1426.3,188.7
LCP-TTFB (median),2017.5,189
LCP-TTFB (median),1452.9,201.3
LCP-TTFB (median),1988.7,205.4
LCP-TTFB (median),1447.1,222.5
LCP-TTFB (median),2042.3,175.5
LCP-TTFB (median),1457,187.9

Notice how the one request in the first column is always extremely slow (10x) compared to the second column, regardless of the optimizations being applied by Optimization Detective. This is because the same browser is being used between the two requests for OD disabled and OD disabled, meaning the second request will be able to reuse cached resources and always be faster.

After

With 10 iterations

for file in "od-disabled-then-od-enabled.txt" "od-enabled-then-od-disabled.txt"; do 
  npm run research -- benchmark-web-vitals -f "$file" -n 10 -o csv -c "Fast 4G"; 
done | tee output.txt

Output:

> research
> ./cli/run.mjs benchmark-web-vitals -f od-disabled-then-od-enabled.txt -n 10 -o csv -c Fast 4G

URL,http://localhost:10079/od-test-page/?optimization_detective_disabled=1,http://localhost:10079/od-test-page/?optimization_detective_enabled=1
Success Rate,100%,100%
FCP (median),402,408
LCP (median),2276,1712
TTFB (median),267.55,264.2
LCP-TTFB (median),2018.15,1451.35


> research
> ./cli/run.mjs benchmark-web-vitals -f od-enabled-then-od-disabled.txt -n 10 -o csv -c Fast 4G

URL,http://localhost:10079/od-test-page/?optimization_detective_enabled=1,http://localhost:10079/od-test-page/?optimization_detective_disabled=1
Success Rate,100%,100%
FCP (median),442,438
LCP (median),1738,2312
TTFB (median),293.05,279.25
LCP-TTFB (median),1445.75,2031.5

Notice now how the OD enabled version is now significantly faster than the OD disabled version (and by the same margin), regardless of whether the OD enabled URL is queried first or the OD disabled URL is queried first.

With 1 iteration over 10 repetitions

for i in {1..10}; do 
  for file in "od-disabled-then-od-enabled.txt" "od-enabled-then-od-disabled.txt"; do 
    npm run research -- benchmark-web-vitals -f "$file" -n 1 -o csv -c "Fast 4G"; 
  done | grep 'LCP-TTFB'; 
done | tee output.txt

Output, where the cells in the first column alternate between OD Disabled and OD Enabled:

LCP-TTFB (median),2043.3,1444.5
LCP-TTFB (median),1412.8,2059.8
LCP-TTFB (median),2019.3,1418.2
LCP-TTFB (median),1450.5,2034.6
LCP-TTFB (median),2004,1450.1
LCP-TTFB (median),1457.7,2025.2
LCP-TTFB (median),2023.8,1440.7
LCP-TTFB (median),1769.3,2038.2
LCP-TTFB (median),2022.1,1442.8
LCP-TTFB (median),1431.1,2003.3
LCP-TTFB (median),2001,1443.7
LCP-TTFB (median),1411.8,2039.6
LCP-TTFB (median),2007.5,1427.4
LCP-TTFB (median),1465.6,2020.3
LCP-TTFB (median),2037.1,1439.7
LCP-TTFB (median),1423.8,1994.4
LCP-TTFB (median),1996.5,1444.4
LCP-TTFB (median),1420.2,2122.2
LCP-TTFB (median),1988.2,1458.6
LCP-TTFB (median),1433.2,2496.4

Notice how the better LCP-TTFB value switches after each row, where the better value corresponds to when the URL had OD enabled. This is in contrast with the before test above where the first column (and the first request) is always much slower than the second column (the second request) regardless of whether the URL had OD enabled or disabled.

Additional Changes

This also will by default make a request to a URL prior to making the first request to collect metric. This is to ensure that the DNS lookups have been cached in the operating system so that the TTFB for the initial request won't be slower than the rest. The --skip-network-priming option can be used to disable this.

Additionally, there is now a --pause-duration <milliseconds> option which can be used to add a delay of the provided milliseconds between each request. This is to give the server a chance to catch its breath, preventing the CPU from getting increasingly taxed which would progressively reflect poorly on TTFB. It's also provided as an option to be a good netizen when benchmarking a site in the field since the rnd query parameter will usually bust page caches.

@westonruter westonruter force-pushed the fix/benchmark-web-vitals-requests branch from a274ecb to 919cbaf Compare April 1, 2025 06:37
@westonruter westonruter marked this pull request as ready for review April 1, 2025 06:44
@westonruter westonruter merged commit 68af1ef into main Apr 1, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants