From bfc451ddf8fd93a99ec5a726f351e0714713c5cb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 25 Oct 2023 11:52:40 -0700
Subject: [PATCH 001/371] Add Image Loading Optimization module
---
admin/server-timing.php | 24 +++++----
.../image-loading-optimization/helper.php | 11 ++++
.../image-loading-optimization/hooks.php | 48 +++++++++++++++++
.../image-loading-optimization/load.php | 17 ++++++
server-timing/class-perflab-server-timing.php | 15 ++++++
tests/admin/server-timing-tests.php | 6 ++-
.../image-loading-optimization/load-tests.php | 54 +++++++++++++++++++
7 files changed, 163 insertions(+), 12 deletions(-)
create mode 100644 modules/images/image-loading-optimization/helper.php
create mode 100644 modules/images/image-loading-optimization/hooks.php
create mode 100644 modules/images/image-loading-optimization/load.php
create mode 100644 tests/modules/images/image-loading-optimization/load-tests.php
diff --git a/admin/server-timing.php b/admin/server-timing.php
index 94a520ec2f..46f28df890 100644
--- a/admin/server-timing.php
+++ b/admin/server-timing.php
@@ -43,16 +43,18 @@ function perflab_add_server_timing_page() {
* @since 2.6.0
*/
function perflab_load_server_timing_page() {
- /*
- * This settings section technically includes a field, however it is directly rendered as part of the section
- * callback due to requiring custom markup.
- */
- add_settings_section(
- 'output-buffering',
- __( 'Output Buffering', 'performance-lab' ),
- 'perflab_render_server_timing_page_output_buffering_section',
- PERFLAB_SERVER_TIMING_SCREEN
- );
+ if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) {
+ /*
+ * This settings section technically includes a field, however it is directly rendered as part of the section
+ * callback due to requiring custom markup.
+ */
+ add_settings_section(
+ 'output-buffering',
+ __( 'Output Buffering', 'performance-lab' ),
+ 'perflab_render_server_timing_page_output_buffering_section',
+ PERFLAB_SERVER_TIMING_SCREEN
+ );
+ }
// Minor style tweaks to improve appearance similar to other core settings screen instances.
add_action(
@@ -93,7 +95,7 @@ static function () {
);
?>
-
+
send_header();
+ return $buffer;
+ },
+ PHP_INT_MAX
+ );
+ return $passthrough;
+ }
+
if ( ! $this->use_output_buffer() ) {
$this->send_header();
return $passthrough;
diff --git a/tests/admin/server-timing-tests.php b/tests/admin/server-timing-tests.php
index 0684ff8409..915da3cdc7 100644
--- a/tests/admin/server-timing-tests.php
+++ b/tests/admin/server-timing-tests.php
@@ -48,8 +48,12 @@ public function test_perflab_load_server_timing_page() {
perflab_load_server_timing_page();
$this->assertArrayHasKey( PERFLAB_SERVER_TIMING_SCREEN, $wp_settings_sections );
+ $expected_sections = array( 'benchmarking' );
+ if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) {
+ $expected_sections[] = 'output-buffering';
+ }
$this->assertEqualSets(
- array( 'output-buffering', 'benchmarking' ),
+ $expected_sections,
array_keys( $wp_settings_sections[ PERFLAB_SERVER_TIMING_SCREEN ] )
);
$this->assertEqualSets(
diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php
new file mode 100644
index 0000000000..a1a9333c05
--- /dev/null
+++ b/tests/modules/images/image-loading-optimization/load-tests.php
@@ -0,0 +1,54 @@
+assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) );
+ }
+
+ /**
+ * Make output is buffered and that it is also filtered.
+ *
+ * @test
+ * @covers ::image_loading_optimization_buffer_output
+ */
+ public function it_buffers_and_filters_output() {
+ $original = 'Hello World!';
+ $expected = '¡Hola Mundo!';
+
+ // In order to test, a wrapping output buffer is required because ob_get_clean() does not invoke the output
+ // buffer callback. See .
+ ob_start();
+
+ add_filter(
+ 'perflab_template_output_buffer',
+ function ( $buffer ) use ( $original, $expected ) {
+ $this->assertSame( $original, $buffer );
+ return $expected;
+ }
+ );
+
+ $original_ob_level = ob_get_level();
+ image_loading_optimization_buffer_output();
+ $this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' );
+ echo $original;
+
+ ob_end_flush(); // Flushing invokes the output buffer callback.
+
+ $buffer = ob_get_clean(); // Get the buffer from our wrapper output buffer.
+ $this->assertSame( $expected, $buffer );
+ }
+}
From e8b3fdad1e7079bc6d2a2202e19bd178bea7b7ba Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 25 Oct 2023 12:41:13 -0700
Subject: [PATCH 002/371] Add Image Loading Optimization to CODEOWNERS
---
.github/CODEOWNERS | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index d2939e8d47..5514f71c0a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -53,3 +53,8 @@
/modules/images/dominant-color-images @pbearne @spacedmonkey
/tests/modules/images/dominant-color-images @pbearne @spacedmonkey
/tests/testdata/modules/images/dominant-color-images @pbearne @spacedmonkey
+
+# Module: Image Loading Optimization
+/modules/images/image-loading-optimization @westonruter
+/tests/modules/images/image-loading-optimization @westonruter
+/tests/testdata/modules/images/image-loading-optimization @westonruter
From 4dcef16be5a8b8e336de4c76c7900d85e7e64072 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 25 Oct 2023 17:26:07 -0700
Subject: [PATCH 003/371] Add skeleton for detection script
---
.../image-loading-optimization/detect.js | 22 +++++++++++++
.../image-loading-optimization/hooks.php | 31 +++++++++++++++++++
2 files changed, 53 insertions(+)
create mode 100644 modules/images/image-loading-optimization/detect.js
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
new file mode 100644
index 0000000000..d7d285bb7e
--- /dev/null
+++ b/modules/images/image-loading-optimization/detect.js
@@ -0,0 +1,22 @@
+/**
+ * Detect the LCP element, loaded images, client viewport and store for future optimizations.
+ *
+ * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
+ * @param {boolean} isDebug Whether to show debug messages.
+ */
+function detect( serveTime, detectionTimeWindow, isDebug ) {
+ const runTime = new Date().valueOf();
+
+ // Abort running detection logic if it was served in a cached page.
+ if ( runTime - serveTime > detectionTimeWindow ) {
+ if ( isDebug ) {
+ console.warn( 'Aborted detection for Image Loading Optimization due to being outside detection time window.' );
+ }
+ return;
+ }
+
+ if ( isDebug ) {
+ console.info('Proceeding with detection for Image Loading Optimization.');
+ }
+}
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 1b2b3de95d..25c56ce706 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -46,3 +46,34 @@ static function ( $output ) {
return $passthrough;
}
add_filter( 'template_include', 'image_loading_optimization_buffer_output', PHP_INT_MAX );
+
+
+/**
+ * Prints the script for detecting loaded images and the LCP element.
+ */
+function image_loading_optimization_print_detection_script() {
+ $serve_time = ceil( microtime( true ) * 1000 );
+
+ /**
+ * Filters the time window between serve time and run time in which loading detection is allowed to run.
+ *
+ * Allow this amount of milliseconds between when the page was first generated (and perhaps cached) and when the
+ * detect function on the page is allowed to perform its detection logic and submit the request to store the results.
+ * This avoids situations in which there is missing detection metrics in which case a site with page caching which
+ * also has a lot of traffic could result in a cache stampede.
+ *
+ * @since n.e.x.t
+ * @todo The value should probably be something like the 99th percentile of TTFB for WordPress sites in CrUX.
+ *
+ * @param int $detection_time_window Detection time window in milliseconds.
+ */
+ $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
+
+ $detect_function = file_get_contents( __DIR__ . '/detect.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ $detect_args = array( $serve_time, $detection_time_window, WP_DEBUG );
+ wp_print_inline_script_tag(
+ sprintf( '( %s )( ...%s )', $detect_function, wp_json_encode( $detect_args ) ),
+ array( 'type' => 'module' )
+ );
+}
+add_action( 'wp_print_footer_scripts', 'image_loading_optimization_print_detection_script' );
From 7ca7e67e15f18fec9d6f6eee0cd85362b7f7df81 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 26 Oct 2023 08:58:55 -0700
Subject: [PATCH 004/371] Update comment to reflect TTLB not TTFB
---
modules/images/image-loading-optimization/hooks.php | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 25c56ce706..26aee6b2e9 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -47,7 +47,6 @@ static function ( $output ) {
}
add_filter( 'template_include', 'image_loading_optimization_buffer_output', PHP_INT_MAX );
-
/**
* Prints the script for detecting loaded images and the LCP element.
*/
@@ -63,7 +62,7 @@ function image_loading_optimization_print_detection_script() {
* also has a lot of traffic could result in a cache stampede.
*
* @since n.e.x.t
- * @todo The value should probably be something like the 99th percentile of TTFB for WordPress sites in CrUX.
+ * @todo The value should probably be something like the 99th percentile of Time To Last Byte (TTLB) for WordPress sites in CrUX.
*
* @param int $detection_time_window Detection time window in milliseconds.
*/
From 721517a902ad056f4066ff7575fbc144870f2564 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 26 Oct 2023 13:42:54 -0700
Subject: [PATCH 005/371] Import module rather than inlining it
---
modules/images/image-loading-optimization/detect.js | 2 +-
modules/images/image-loading-optimization/hooks.php | 9 ++++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index d7d285bb7e..e4cab9ab68 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -5,7 +5,7 @@
* @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
* @param {boolean} isDebug Whether to show debug messages.
*/
-function detect( serveTime, detectionTimeWindow, isDebug ) {
+export default async function detect( serveTime, detectionTimeWindow, isDebug ) {
const runTime = new Date().valueOf();
// Abort running detection logic if it was served in a cached page.
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 26aee6b2e9..09692aa828 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -68,10 +68,13 @@ function image_loading_optimization_print_detection_script() {
*/
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
- $detect_function = file_get_contents( __DIR__ . '/detect.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
- $detect_args = array( $serve_time, $detection_time_window, WP_DEBUG );
+ $detect_args = array( $serve_time, $detection_time_window, WP_DEBUG );
wp_print_inline_script_tag(
- sprintf( '( %s )( ...%s )', $detect_function, wp_json_encode( $detect_args ) ),
+ sprintf(
+ 'import detect from %s; detect( ...%s )',
+ wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detect.js' ) ),
+ wp_json_encode( $detect_args )
+ ),
array( 'type' => 'module' )
);
}
From e2212a870821cf096d6460dedc850cd9e673b349 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 30 Oct 2023 12:19:54 -0700
Subject: [PATCH 006/371] WIP
---
.eslintrc.js | 7 +
.../image-loading-optimization/detect.js | 140 +++++++++++++++++-
2 files changed, 141 insertions(+), 6 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 86eba448b8..eb23001613 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -8,7 +8,14 @@ const config = {
rules: {
...( wpConfig?.rules || {} ),
'jsdoc/valid-types': 'off',
+ 'no-console': 'off',
},
+ env: {
+ 'browser': true,
+ },
+ globals: {
+ scheduler: false,
+ }
};
module.exports = config;
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index e4cab9ab68..f4dc98bb1c 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -1,22 +1,150 @@
+/**
+ * External dependencies
+ */
+
+/** @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution */
+
+/**
+ * Yield to the main thread.
+ *
+ * @see https://developer.chrome.com/blog/introducing-scheduler-yield-origin-trial/#enter-scheduleryield
+ * @return {Promise}
+ */
+function yieldToMain() {
+ /** @type */
+ if (
+ typeof scheduler !== 'undefined' &&
+ typeof scheduler.yield === 'function'
+ ) {
+ return scheduler.yield();
+ }
+
+ // Fall back to setTimeout:
+ return new Promise( ( resolve ) => {
+ setTimeout( resolve, 0 );
+ } );
+}
+
/**
* Detect the LCP element, loaded images, client viewport and store for future optimizations.
*
- * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
- * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
- * @param {boolean} isDebug Whether to show debug messages.
+ * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
+ * @param {boolean} isDebug Whether to show debug messages.
*/
-export default async function detect( serveTime, detectionTimeWindow, isDebug ) {
+export default async function detect(
+ serveTime,
+ detectionTimeWindow,
+ isDebug
+) {
const runTime = new Date().valueOf();
// Abort running detection logic if it was served in a cached page.
if ( runTime - serveTime > detectionTimeWindow ) {
if ( isDebug ) {
- console.warn( 'Aborted detection for Image Loading Optimization due to being outside detection time window.' );
+ console.warn(
+ 'Aborted detection for Image Loading Optimization due to being outside detection time window.'
+ );
}
return;
}
if ( isDebug ) {
- console.info('Proceeding with detection for Image Loading Optimization.');
+ console.info(
+ 'Proceeding with detection for Image Loading Optimization.'
+ );
+ }
+
+ // TODO: Use a local copy of web-vitals.
+ const { onLCP } = await import(
+ // eslint-disable-next-line import/no-unresolved
+ 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module'
+ );
+
+ // const perfObserver = new PerformanceObserver( ( list ) => {
+ // const entries = list.getEntries();
+ // for ( const entry of entries ) {
+ // console.log( 'perfObserver LCP:', entry );
+ // }
+ // } );
+ //
+ // perfObserver.observe( {
+ // type: 'largest-contentful-paint',
+ // buffered: true,
+ // } );
+
+ /** @type {LCPMetricWithAttribution[]} */
+ const lcpCandidates = [];
+
+ // TODO: Obtain other candidates than the LCP? If the LCP is text and there's an image too, we should add fetchpriority to the image still even though it isn't LCP.
+ const lcpCandidateObtained = new Promise( ( resolve ) => {
+ onLCP(
+ ( metric ) => {
+ lcpCandidates.push( metric );
+ resolve();
+ },
+ {
+ // This avoids needing to click to finalize LCP candidate. While this is helpful for testing, it also
+ // ensures that we always get an LCP candidate reported. Otherwise, the callback may never fire if the
+ // user never does a click or keydown, per .
+ reportAllChanges: true,
+ }
+ );
+ } );
+
+ // Note: We cannot use the window load event because the module may load after it fires.
+
+ // To watch for intersection relative to the device's viewport, specify null for the root option.
+ console.info( {
+ viewportWidth: window.innerWidth,
+ viewportHeight: window.innerHeight,
+ } );
+
+ const options = {
+ root: null,
+ // rootMargin: "0px",
+ threshold: 0.0, // As soon as even one pixel is visible.
+ };
+
+ const adminBar = document.getElementById( 'wpadminbar' );
+ const imageObserver = new IntersectionObserver( ( entries ) => {
+ for ( const entry of entries ) {
+ if (
+ entry.isIntersecting &&
+ ( ! adminBar || ! adminBar.contains( entry.target ) )
+ ) {
+ console.info( 'Initial image:', entry.target );
+ }
+ }
+ }, options );
+ for ( const img of document.getElementsByTagName( 'img' ) ) {
+ imageObserver.observe( img );
+ }
+
+ // Wait until we have an LCP candidate, although more may come upon the page finishing loading.
+ await lcpCandidateObtained;
+
+ // Wait until the page has fully loaded. Note that a module is delayed like a script with defer.
+ await new Promise( ( resolve ) => {
+ if ( document.readyState === 'complete' ) {
+ resolve();
+ } else {
+ window.addEventListener( 'load', resolve, { once: true } );
+ }
+ } );
+
+ // Wait for an additional timer.
+ // await new Promise( ( resolve ) => {
+ // setTimeout( resolve, 1000 ); // TODO: What time makes sense?
+ // } );
+
+ // Stop observing.
+ imageObserver.disconnect();
+ if ( isDebug ) {
+ console.info( 'Detection is stopping.' );
}
+
+ console.info( 'lcpCandidates', lcpCandidates );
+
+ // TODO: Send data to server.
}
From 37a603cfd3a87978ab77ca77148c8383350194a4 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 30 Oct 2023 12:27:16 -0700
Subject: [PATCH 007/371] Improve logging and remove obsolete code
---
.../image-loading-optimization/detect.js | 49 +++++++------------
1 file changed, 17 insertions(+), 32 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index f4dc98bb1c..836723cdaf 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -1,9 +1,15 @@
-/**
- * External dependencies
- */
-
/** @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution */
+const consoleLogPrefix = '[Image Loading Optimization]';
+
+function log( ...message ) {
+ console.log( consoleLogPrefix, ...message );
+}
+
+function warn( ...message ) {
+ console.warn( consoleLogPrefix, ...message );
+}
+
/**
* Yield to the main thread.
*
@@ -42,7 +48,7 @@ export default async function detect(
// Abort running detection logic if it was served in a cached page.
if ( runTime - serveTime > detectionTimeWindow ) {
if ( isDebug ) {
- console.warn(
+ warn(
'Aborted detection for Image Loading Optimization due to being outside detection time window.'
);
}
@@ -50,9 +56,7 @@ export default async function detect(
}
if ( isDebug ) {
- console.info(
- 'Proceeding with detection for Image Loading Optimization.'
- );
+ log( 'Proceeding with detection' );
}
// TODO: Use a local copy of web-vitals.
@@ -61,22 +65,10 @@ export default async function detect(
'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module'
);
- // const perfObserver = new PerformanceObserver( ( list ) => {
- // const entries = list.getEntries();
- // for ( const entry of entries ) {
- // console.log( 'perfObserver LCP:', entry );
- // }
- // } );
- //
- // perfObserver.observe( {
- // type: 'largest-contentful-paint',
- // buffered: true,
- // } );
-
/** @type {LCPMetricWithAttribution[]} */
const lcpCandidates = [];
- // TODO: Obtain other candidates than the LCP? If the LCP is text and there's an image too, we should add fetchpriority to the image still even though it isn't LCP.
+ // Obtain at least one LCP candidate.
const lcpCandidateObtained = new Promise( ( resolve ) => {
onLCP(
( metric ) => {
@@ -92,10 +84,8 @@ export default async function detect(
);
} );
- // Note: We cannot use the window load event because the module may load after it fires.
-
// To watch for intersection relative to the device's viewport, specify null for the root option.
- console.info( {
+ log( {
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
} );
@@ -113,7 +103,7 @@ export default async function detect(
entry.isIntersecting &&
( ! adminBar || ! adminBar.contains( entry.target ) )
) {
- console.info( 'Initial image:', entry.target );
+ log( 'Initial image:', entry.target );
}
}
}, options );
@@ -133,18 +123,13 @@ export default async function detect(
}
} );
- // Wait for an additional timer.
- // await new Promise( ( resolve ) => {
- // setTimeout( resolve, 1000 ); // TODO: What time makes sense?
- // } );
-
// Stop observing.
imageObserver.disconnect();
if ( isDebug ) {
- console.info( 'Detection is stopping.' );
+ log( 'Detection is stopping.' );
}
- console.info( 'lcpCandidates', lcpCandidates );
+ log( 'lcpCandidates', lcpCandidates );
// TODO: Send data to server.
}
From 31f5ffe7b1eebec8aea315fb1cc9058e320cbd3e Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 30 Oct 2023 15:36:04 -0700
Subject: [PATCH 008/371] WIP2
---
.../image-loading-optimization/detect.js | 95 ++++++++++++++-----
package-lock.json | 13 +++
package.json | 3 +
3 files changed, 87 insertions(+), 24 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 836723cdaf..24c271fe4d 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -31,6 +31,36 @@ function yieldToMain() {
} );
}
+/**
+ * @typedef {Object} Breadcrumb
+ * @property {number} index
+ * @property {string} tagName
+ */
+
+/**
+ * Gets breadcrumbs for a given element.
+ *
+ * @param {Element} element
+ * @return {Breadcrumb[]} Breadcrumbs.
+ */
+function getBreadcrumbs( element ) {
+ /** @type {Breadcrumb[]} */
+ const breadcrumbs = [];
+
+ let node = element;
+ while ( node instanceof Element ) {
+ breadcrumbs.unshift( {
+ tagName: node.tagName,
+ index: node.parentElement
+ ? Array.from( node.parentElement.children ).indexOf( node )
+ : 0,
+ } );
+ node = node.parentElement;
+ }
+
+ return breadcrumbs;
+}
+
/**
* Detect the LCP element, loaded images, client viewport and store for future optimizations.
*
@@ -59,6 +89,14 @@ export default async function detect(
log( 'Proceeding with detection' );
}
+ const results = {
+ viewport: {
+ width: window.innerWidth,
+ height: window.innerHeight,
+ },
+ images: [],
+ };
+
// TODO: Use a local copy of web-vitals.
const { onLCP } = await import(
// eslint-disable-next-line import/no-unresolved
@@ -66,13 +104,13 @@ export default async function detect(
);
/** @type {LCPMetricWithAttribution[]} */
- const lcpCandidates = [];
+ const lcpMetricCandidates = [];
// Obtain at least one LCP candidate.
const lcpCandidateObtained = new Promise( ( resolve ) => {
onLCP(
( metric ) => {
- lcpCandidates.push( metric );
+ lcpMetricCandidates.push( metric );
resolve();
},
{
@@ -84,31 +122,29 @@ export default async function detect(
);
} );
- // To watch for intersection relative to the device's viewport, specify null for the root option.
- log( {
- viewportWidth: window.innerWidth,
- viewportHeight: window.innerHeight,
- } );
-
- const options = {
- root: null,
- // rootMargin: "0px",
- threshold: 0.0, // As soon as even one pixel is visible.
- };
+ /** @type {IntersectionObserverEntry[]} */
+ const imageIntersections = [];
- const adminBar = document.getElementById( 'wpadminbar' );
- const imageObserver = new IntersectionObserver( ( entries ) => {
- for ( const entry of entries ) {
- if (
- entry.isIntersecting &&
- ( ! adminBar || ! adminBar.contains( entry.target ) )
- ) {
- log( 'Initial image:', entry.target );
+ const imageObserver = new IntersectionObserver(
+ ( entries ) => {
+ for ( const entry of entries ) {
+ //if ( entry.isIntersecting ) {
+ console.info( 'interesecting!', entry );
+ imageIntersections.push( entry );
+ //}
}
+ },
+ {
+ root: null, // To watch for intersection relative to the device's viewport, specify null for the root option.
+ threshold: 0.0, // As soon as even one pixel is visible.
}
- }, options );
+ );
+
+ const adminBar = document.getElementById( 'wpadminbar' );
for ( const img of document.getElementsByTagName( 'img' ) ) {
- imageObserver.observe( img );
+ if ( ! adminBar || ! adminBar.contains( img ) ) {
+ imageObserver.observe( img );
+ }
}
// Wait until we have an LCP candidate, although more may come upon the page finishing loading.
@@ -129,7 +165,18 @@ export default async function detect(
log( 'Detection is stopping.' );
}
- log( 'lcpCandidates', lcpCandidates );
+ console.info( imageIntersections );
+ const lcpMetric = lcpMetricCandidates.at( -1 );
+ for ( const imageIntersection of imageIntersections ) {
+ log(
+ 'imageIntersection.target',
+ imageIntersection.target,
+ getBreadcrumbs( imageIntersection.target )
+ );
+ }
+ // lcpMetric.attribution.element
+
+ log( 'lcpCandidates', lcpMetricCandidates );
// TODO: Send data to server.
}
diff --git a/package-lock.json b/package-lock.json
index a324943f09..f5354ef3ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,9 @@
"": {
"name": "performance",
"license": "GPL-2.0-or-later",
+ "dependencies": {
+ "web-vitals": "3.5.0"
+ },
"devDependencies": {
"@octokit/rest": "^19.0.5",
"@wordpress/env": "^5.7.0",
@@ -17103,6 +17106,11 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/web-vitals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz",
+ "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w=="
+ },
"node_modules/webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
@@ -30546,6 +30554,11 @@
"defaults": "^1.0.3"
}
},
+ "web-vitals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz",
+ "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w=="
+ },
"webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
diff --git a/package.json b/package.json
index d88d3f9bf8..d111960285 100644
--- a/package.json
+++ b/package.json
@@ -45,5 +45,8 @@
"composer run-script lint",
"composer run-script phpstan"
]
+ },
+ "dependencies": {
+ "web-vitals": "3.5.0"
}
}
From d700943939267121bd01e511771da5381f5ac9b7 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 30 Oct 2023 15:52:35 -0700
Subject: [PATCH 009/371] WIP3
---
.../image-loading-optimization/detect.js | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 24c271fe4d..bc78cc2ec4 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -126,12 +126,16 @@ export default async function detect(
const imageIntersections = [];
const imageObserver = new IntersectionObserver(
- ( entries ) => {
+ ( entries, observer ) => {
+ consl.info('callback!');
for ( const entry of entries ) {
- //if ( entry.isIntersecting ) {
- console.info( 'interesecting!', entry );
- imageIntersections.push( entry );
- //}
+ if ( entry.isIntersecting ) {
+ console.info( 'interesecting!', entry );
+ imageIntersections.push( entry );
+ } else {
+ console.info( 'npt interesecting!', entry );
+ }
+ observer.unobserve( entry.target );
}
},
{
@@ -141,8 +145,10 @@ export default async function detect(
);
const adminBar = document.getElementById( 'wpadminbar' );
- for ( const img of document.getElementsByTagName( 'img' ) ) {
+ const imgCollection = document.body.getElementsByTagName( 'img' );
+ for ( /** @type {HTMLImageElement} */ const img of imgCollection ) {
if ( ! adminBar || ! adminBar.contains( img ) ) {
+ console.info( 'observe', img );
imageObserver.observe( img );
}
}
@@ -179,4 +185,5 @@ export default async function detect(
log( 'lcpCandidates', lcpMetricCandidates );
// TODO: Send data to server.
+ log( results );
}
From e9aee7ea9b023ac34f86b7ab5a2eb0bdba0fc445 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 30 Oct 2023 16:52:39 -0700
Subject: [PATCH 010/371] WIP4
---
.../image-loading-optimization/detect.js | 41 +++++++++++++------
1 file changed, 28 insertions(+), 13 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index bc78cc2ec4..4992764743 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -17,7 +17,6 @@ function warn( ...message ) {
* @return {Promise}
*/
function yieldToMain() {
- /** @type */
if (
typeof scheduler !== 'undefined' &&
typeof scheduler.yield === 'function'
@@ -122,24 +121,28 @@ export default async function detect(
);
} );
+ // Ensure the DOM is loaded (although it surely already is since we're executing in a module).
+ await new Promise( ( resolve ) => {
+ if ( document.readyState !== 'loading' ) {
+ resolve();
+ } else {
+ document.addEventListener( 'DOMContentLoaded', resolve );
+ }
+ } );
+
/** @type {IntersectionObserverEntry[]} */
const imageIntersections = [];
const imageObserver = new IntersectionObserver(
- ( entries, observer ) => {
- consl.info('callback!');
+ ( entries ) => {
for ( const entry of entries ) {
if ( entry.isIntersecting ) {
- console.info( 'interesecting!', entry );
imageIntersections.push( entry );
- } else {
- console.info( 'npt interesecting!', entry );
}
- observer.unobserve( entry.target );
}
},
{
- root: null, // To watch for intersection relative to the device's viewport, specify null for the root option.
+ root: null, // To watch for intersection relative to the device's viewport.
threshold: 0.0, // As soon as even one pixel is visible.
}
);
@@ -148,7 +151,6 @@ export default async function detect(
const imgCollection = document.body.getElementsByTagName( 'img' );
for ( /** @type {HTMLImageElement} */ const img of imgCollection ) {
if ( ! adminBar || ! adminBar.contains( img ) ) {
- console.info( 'observe', img );
imageObserver.observe( img );
}
}
@@ -156,7 +158,7 @@ export default async function detect(
// Wait until we have an LCP candidate, although more may come upon the page finishing loading.
await lcpCandidateObtained;
- // Wait until the page has fully loaded. Note that a module is delayed like a script with defer.
+ // Wait until the images on the page have fully loaded.
await new Promise( ( resolve ) => {
if ( document.readyState === 'complete' ) {
resolve();
@@ -165,22 +167,35 @@ export default async function detect(
}
} );
+ // Give the image intersection observer a chance to report back.
+ // TODO: This needs to be hardened. How long to wait for callback? What about when there are no images in the page?
+ await new Promise( async ( resolve ) => {
+ if ( window.requestIdleCallback ) {
+ window.requestIdleCallback( resolve );
+ } else {
+ setTimeout( resolve, 1 );
+ }
+ } );
+
// Stop observing.
imageObserver.disconnect();
if ( isDebug ) {
log( 'Detection is stopping.' );
}
- console.info( imageIntersections );
const lcpMetric = lcpMetricCandidates.at( -1 );
for ( const imageIntersection of imageIntersections ) {
log(
'imageIntersection.target',
imageIntersection.target,
- getBreadcrumbs( imageIntersection.target )
+ getBreadcrumbs( imageIntersection.target ),
+ lcpMetric &&
+ imageIntersection.target ===
+ lcpMetric.attribution.lcpEntry.element
+ ? 'is LCP'
+ : 'is NOT LCP'
);
}
- // lcpMetric.attribution.element
log( 'lcpCandidates', lcpMetricCandidates );
From 685c25e5938a793a021b03b07dcc34599bfa17ce Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 31 Oct 2023 10:34:57 -0700
Subject: [PATCH 011/371] Capture image breadcrumbs early; improve waiting for
intersection observer
---
.../image-loading-optimization/detect.js | 137 +++++++++++-------
1 file changed, 81 insertions(+), 56 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 4992764743..c9bf335957 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -32,8 +32,14 @@ function yieldToMain() {
/**
* @typedef {Object} Breadcrumb
- * @property {number} index
- * @property {string} tagName
+ * @property {number} index - Index of element among sibling elements.
+ * @property {string} tagName - Tag name.
+ */
+
+/**
+ * @typedef {Object} ElementBreadcrumb
+ * @property {Element} element - Element node.
+ * @property {Breadcrumb} breadcrumb - Breadcrumb for the element.
*/
/**
@@ -73,6 +79,8 @@ export default async function detect(
isDebug
) {
const runTime = new Date().valueOf();
+ const doc = document;
+ const win = window;
// Abort running detection logic if it was served in a cached page.
if ( runTime - serveTime > detectionTimeWindow ) {
@@ -88,14 +96,76 @@ export default async function detect(
log( 'Proceeding with detection' );
}
+ // Obtain the admin bar element because we don't want to detect elements inside of it.
+ const adminBar =
+ /** @type {?HTMLDivElement} */ doc.getElementById( 'wpadminbar' );
+
+ // Note that we capture an array of image elements because getElementsByTagName() returns a live HTMLCollection.
+ // We also need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
+ // mutating the DOM from the original HTML rendered by the server, in which case the breadcrumbs obtained from the
+ // client will no longer be valid on the server.
+ const breadcrumbedImages = /** @type {ElementBreadcrumb[]} */ Array.from(
+ doc.body.getElementsByTagName( 'img' )
+ ).map( ( img ) => {
+ return {
+ element: img,
+ breadcrumb: getBreadcrumbs( img ),
+ };
+ } );
+
const results = {
viewport: {
- width: window.innerWidth,
- height: window.innerHeight,
+ width: win.innerWidth,
+ height: win.innerHeight,
},
images: [],
};
+ // Ensure the DOM is loaded (although it surely already is since we're executing in a module).
+ await new Promise( ( resolve ) => {
+ if ( doc.readyState !== 'loading' ) {
+ resolve();
+ } else {
+ doc.addEventListener( 'DOMContentLoaded', resolve, { once: true } );
+ }
+ } );
+
+ /** @type {IntersectionObserverEntry[]} */
+ const imageIntersections = [];
+
+ /** @type {?IntersectionObserver} */
+ let imageObserver;
+
+ // Wait for the intersection observer to report back on the initially-visible images.
+ // Note that the first callback will include _all_ observed entries per .
+ if ( breadcrumbedImages.length > 0 ) {
+ await new Promise( ( resolve ) => {
+ imageObserver = new IntersectionObserver(
+ ( entries ) => {
+ for ( const entry of entries ) {
+ if ( entry.isIntersecting ) {
+ imageIntersections.push( entry );
+ }
+ }
+ resolve();
+ },
+ {
+ root: null, // To watch for intersection relative to the device's viewport.
+ threshold: 0.0, // As soon as even one pixel is visible.
+ }
+ );
+
+ for ( const breadcrumbedImage of breadcrumbedImages ) {
+ if (
+ ! adminBar ||
+ ! adminBar.contains( breadcrumbedImage.element )
+ ) {
+ imageObserver.observe( breadcrumbedImage.element );
+ }
+ }
+ } );
+ }
+
// TODO: Use a local copy of web-vitals.
const { onLCP } = await import(
// eslint-disable-next-line import/no-unresolved
@@ -105,8 +175,8 @@ export default async function detect(
/** @type {LCPMetricWithAttribution[]} */
const lcpMetricCandidates = [];
- // Obtain at least one LCP candidate.
- const lcpCandidateObtained = new Promise( ( resolve ) => {
+ // Obtain at least one LCP candidate. More may be reported before the page finishes loading.
+ await new Promise( ( resolve ) => {
onLCP(
( metric ) => {
lcpMetricCandidates.push( metric );
@@ -121,64 +191,19 @@ export default async function detect(
);
} );
- // Ensure the DOM is loaded (although it surely already is since we're executing in a module).
- await new Promise( ( resolve ) => {
- if ( document.readyState !== 'loading' ) {
- resolve();
- } else {
- document.addEventListener( 'DOMContentLoaded', resolve );
- }
- } );
-
- /** @type {IntersectionObserverEntry[]} */
- const imageIntersections = [];
-
- const imageObserver = new IntersectionObserver(
- ( entries ) => {
- for ( const entry of entries ) {
- if ( entry.isIntersecting ) {
- imageIntersections.push( entry );
- }
- }
- },
- {
- root: null, // To watch for intersection relative to the device's viewport.
- threshold: 0.0, // As soon as even one pixel is visible.
- }
- );
-
- const adminBar = document.getElementById( 'wpadminbar' );
- const imgCollection = document.body.getElementsByTagName( 'img' );
- for ( /** @type {HTMLImageElement} */ const img of imgCollection ) {
- if ( ! adminBar || ! adminBar.contains( img ) ) {
- imageObserver.observe( img );
- }
- }
-
- // Wait until we have an LCP candidate, although more may come upon the page finishing loading.
- await lcpCandidateObtained;
-
// Wait until the images on the page have fully loaded.
await new Promise( ( resolve ) => {
- if ( document.readyState === 'complete' ) {
+ if ( doc.readyState === 'complete' ) {
resolve();
} else {
- window.addEventListener( 'load', resolve, { once: true } );
- }
- } );
-
- // Give the image intersection observer a chance to report back.
- // TODO: This needs to be hardened. How long to wait for callback? What about when there are no images in the page?
- await new Promise( async ( resolve ) => {
- if ( window.requestIdleCallback ) {
- window.requestIdleCallback( resolve );
- } else {
- setTimeout( resolve, 1 );
+ win.addEventListener( 'load', resolve, { once: true } );
}
} );
// Stop observing.
- imageObserver.disconnect();
+ if ( imageObserver ) {
+ imageObserver.disconnect();
+ }
if ( isDebug ) {
log( 'Detection is stopping.' );
}
From 6d90d75f11b4a9f905f20ed4497c125f5e871beb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 31 Oct 2023 10:58:02 -0700
Subject: [PATCH 012/371] Add getBreadcrumbedElements() function
---
.../image-loading-optimization/detect.js | 40 +++++++++++++------
1 file changed, 28 insertions(+), 12 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index c9bf335957..4491cd24d7 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -2,6 +2,9 @@
const consoleLogPrefix = '[Image Loading Optimization]';
+const win = window;
+const doc = win.document;
+
function log( ...message ) {
console.log( consoleLogPrefix, ...message );
}
@@ -38,9 +41,31 @@ function yieldToMain() {
/**
* @typedef {Object} ElementBreadcrumb
- * @property {Element} element - Element node.
- * @property {Breadcrumb} breadcrumb - Breadcrumb for the element.
+ * @property {Element} element - Element node.
+ * @property {Breadcrumb[]} breadcrumbs - Breadcrumb for the element.
+ */
+
+/**
+ * Get breadcrumbed elements.
+ *
+ * @param {string} selector
+ * @return {ElementBreadcrumb[]} Breadcrumbed elements.
*/
+function getBreadcrumbedElements( selector ) {
+ /** @type {ElementBreadcrumb[]} */
+ const breadcrumbedElements = [];
+
+ /** @type {HTMLCollection} */
+ const elements = doc.body.querySelectorAll( selector );
+ for ( const element of elements ) {
+ breadcrumbedElements.push( {
+ element,
+ breadcrumb: getBreadcrumbs( element ),
+ } );
+ }
+
+ return breadcrumbedElements;
+}
/**
* Gets breadcrumbs for a given element.
@@ -79,8 +104,6 @@ export default async function detect(
isDebug
) {
const runTime = new Date().valueOf();
- const doc = document;
- const win = window;
// Abort running detection logic if it was served in a cached page.
if ( runTime - serveTime > detectionTimeWindow ) {
@@ -104,14 +127,7 @@ export default async function detect(
// We also need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
// mutating the DOM from the original HTML rendered by the server, in which case the breadcrumbs obtained from the
// client will no longer be valid on the server.
- const breadcrumbedImages = /** @type {ElementBreadcrumb[]} */ Array.from(
- doc.body.getElementsByTagName( 'img' )
- ).map( ( img ) => {
- return {
- element: img,
- breadcrumb: getBreadcrumbs( img ),
- };
- } );
+ const breadcrumbedImages = getBreadcrumbedElements( 'img' );
const results = {
viewport: {
From 00be1c72d66ab6d09a7ce7b989d38e56cf56c7a1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 31 Oct 2023 11:10:36 -0700
Subject: [PATCH 013/371] Add missing lint-js to lint-staged and update
lint-js/format-js to find all JS files
---
.eslintrc.js | 6 ++++--
.gitignore | 3 +++
package.json | 7 +++++--
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index eb23001613..66fac527a8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -11,11 +11,13 @@ const config = {
'no-console': 'off',
},
env: {
- 'browser': true,
+ browser: true,
},
globals: {
scheduler: false,
- }
+ },
+ // Note: The '/wp-*' pattern is to ignore symlinks which may be added for local development.
+ ignorePatterns: [ '/vendor', '/node_modules', '/wp-*' ],
};
module.exports = config;
diff --git a/.gitignore b/.gitignore
index 1889d02de2..6d54c0c132 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,6 @@ temp/
._*
.Trashes
.svn
+
+# Possible symlinks to wp-env install-path directories.
+/wp-*
diff --git a/package.json b/package.json
index d111960285..1518df73c7 100644
--- a/package.json
+++ b/package.json
@@ -27,8 +27,8 @@
"test-plugins": "./bin/plugin/cli.js test-plugins",
"test-plugins-multisite": "./bin/plugin/cli.js test-plugins --sitetype=multi",
"enabled-modules": "./bin/plugin/cli.js enabled-modules",
- "format-js": "wp-scripts format ./bin",
- "lint-js": "wp-scripts lint-js ./bin",
+ "format-js": "wp-scripts format",
+ "lint-js": "wp-scripts lint-js",
"format-php": "wp-env run composer run-script format",
"phpstan": "wp-env run composer run-script phpstan",
"prelint-php": "wp-env run composer 'install --no-interaction'",
@@ -44,6 +44,9 @@
"*.php": [
"composer run-script lint",
"composer run-script phpstan"
+ ],
+ "*.js": [
+ "npm run lint-js"
]
},
"dependencies": {
From c9518a1ac58d297493fa1d3b40b9699de03dcaa6 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 31 Oct 2023 11:12:30 -0700
Subject: [PATCH 014/371] Remove yet unused yieldToMain
---
.../image-loading-optimization/detect.js | 20 -------------------
1 file changed, 20 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 4491cd24d7..9183e2e5ce 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -13,26 +13,6 @@ function warn( ...message ) {
console.warn( consoleLogPrefix, ...message );
}
-/**
- * Yield to the main thread.
- *
- * @see https://developer.chrome.com/blog/introducing-scheduler-yield-origin-trial/#enter-scheduleryield
- * @return {Promise}
- */
-function yieldToMain() {
- if (
- typeof scheduler !== 'undefined' &&
- typeof scheduler.yield === 'function'
- ) {
- return scheduler.yield();
- }
-
- // Fall back to setTimeout:
- return new Promise( ( resolve ) => {
- setTimeout( resolve, 0 );
- } );
-}
-
/**
* @typedef {Object} Breadcrumb
* @property {number} index - Index of element among sibling elements.
From 898417691aba919c2d67bbb5738b87483f787feb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 13:33:11 -0700
Subject: [PATCH 015/371] Prevent detection if page is not scrolled to top
---
modules/images/image-loading-optimization/detect.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 9183e2e5ce..c173f87e12 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -89,7 +89,17 @@ export default async function detect(
if ( runTime - serveTime > detectionTimeWindow ) {
if ( isDebug ) {
warn(
- 'Aborted detection for Image Loading Optimization due to being outside detection time window.'
+ 'Aborted detection due to being outside detection time window.'
+ );
+ }
+ return;
+ }
+
+ // Prevent detection when page is not scrolled to the initial viewport.
+ if ( doc.documentElement.scrollTop > 0 ) {
+ if ( isDebug ) {
+ warn(
+ 'Aborted detection since initial scroll position of page is not at the top.'
);
}
return;
From 62be41bc48cf940d22f05d523606c47ae7a30c4a Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 13:33:53 -0700
Subject: [PATCH 016/371] Stop observing images as soon as scroll happens or
detection is stopped
---
.../images/image-loading-optimization/detect.js | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index c173f87e12..33f8170fbb 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -142,6 +142,13 @@ export default async function detect(
/** @type {?IntersectionObserver} */
let imageObserver;
+ function disconnectImageObserver() {
+ if ( imageObserver instanceof IntersectionObserver ) {
+ imageObserver.disconnect();
+ win.removeEventListener( 'scroll', disconnectImageObserver ); // Clean up, even though this is registered with once:true.
+ }
+ }
+
// Wait for the intersection observer to report back on the initially-visible images.
// Note that the first callback will include _all_ observed entries per .
if ( breadcrumbedImages.length > 0 ) {
@@ -170,6 +177,12 @@ export default async function detect(
}
}
} );
+
+ // Stop observing images as soon as the page scrolls since we only want initial-viewport images.
+ win.addEventListener( 'scroll', disconnectImageObserver, {
+ once: true,
+ passive: true,
+ } );
}
// TODO: Use a local copy of web-vitals.
@@ -207,9 +220,7 @@ export default async function detect(
} );
// Stop observing.
- if ( imageObserver ) {
- imageObserver.disconnect();
- }
+ disconnectImageObserver();
if ( isDebug ) {
log( 'Detection is stopping.' );
}
From eb1d93fa581280afff9866a820b5a0a1d6857989 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 13:59:50 -0700
Subject: [PATCH 017/371] Extend observation to elements with background images
---
.../image-loading-optimization/detect.js | 81 +++++++++++--------
1 file changed, 48 insertions(+), 33 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 33f8170fbb..07c8efb7c4 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -20,7 +20,7 @@ function warn( ...message ) {
*/
/**
- * @typedef {Object} ElementBreadcrumb
+ * @typedef {Object} ElementBreadcrumbs
* @property {Element} element - Element node.
* @property {Breadcrumb[]} breadcrumbs - Breadcrumb for the element.
*/
@@ -28,19 +28,18 @@ function warn( ...message ) {
/**
* Get breadcrumbed elements.
*
- * @param {string} selector
- * @return {ElementBreadcrumb[]} Breadcrumbed elements.
+ * @param {HTMLCollection|Element[]} elements Elements.
+ * @return {ElementBreadcrumbs[]} Breadcrumbed elements.
*/
-function getBreadcrumbedElements( selector ) {
- /** @type {ElementBreadcrumb[]} */
+function getBreadcrumbedElements( elements ) {
+ /** @type {ElementBreadcrumbs[]} */
const breadcrumbedElements = [];
/** @type {HTMLCollection} */
- const elements = doc.body.querySelectorAll( selector );
for ( const element of elements ) {
breadcrumbedElements.push( {
element,
- breadcrumb: getBreadcrumbs( element ),
+ breadcrumbs: getBreadcrumbs( element ),
} );
}
@@ -96,6 +95,7 @@ export default async function detect(
}
// Prevent detection when page is not scrolled to the initial viewport.
+ // TODO: Does this cause layout/reflow? https://gist.github.com/paulirish/5d52fb081b3570c81e3a
if ( doc.documentElement.scrollTop > 0 ) {
if ( isDebug ) {
warn(
@@ -113,11 +113,26 @@ export default async function detect(
const adminBar =
/** @type {?HTMLDivElement} */ doc.getElementById( 'wpadminbar' );
- // Note that we capture an array of image elements because getElementsByTagName() returns a live HTMLCollection.
- // We also need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
+ // We need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
// mutating the DOM from the original HTML rendered by the server, in which case the breadcrumbs obtained from the
- // client will no longer be valid on the server.
- const breadcrumbedImages = getBreadcrumbedElements( 'img' );
+ // client will no longer be valid on the server. As such, the results are stored in an array and not any live list.
+ const breadcrumbedImages = getBreadcrumbedElements(
+ doc.body.querySelectorAll( 'img' )
+ );
+
+ // We do the same for elements with background images which are not data: URLs.
+ const breadcrumbedElementsWithBackgrounds = getBreadcrumbedElements(
+ Array.from(
+ doc.body.querySelectorAll( '[style*="background"]' )
+ ).filter( ( /** @type {Element} */ el ) =>
+ /url\(\s*['"](?!=data:)/.test( el.style.backgroundImage )
+ )
+ );
+
+ const breadcrumbedOptimizableElements = [
+ ...breadcrumbedImages,
+ ...breadcrumbedElementsWithBackgrounds,
+ ];
const results = {
viewport: {
@@ -137,27 +152,27 @@ export default async function detect(
} );
/** @type {IntersectionObserverEntry[]} */
- const imageIntersections = [];
+ const elementIntersections = [];
/** @type {?IntersectionObserver} */
- let imageObserver;
+ let intersectionObserver;
- function disconnectImageObserver() {
- if ( imageObserver instanceof IntersectionObserver ) {
- imageObserver.disconnect();
- win.removeEventListener( 'scroll', disconnectImageObserver ); // Clean up, even though this is registered with once:true.
+ function disconnectIntersectionObserver() {
+ if ( intersectionObserver instanceof IntersectionObserver ) {
+ intersectionObserver.disconnect();
+ win.removeEventListener( 'scroll', disconnectIntersectionObserver ); // Clean up, even though this is registered with once:true.
}
}
- // Wait for the intersection observer to report back on the initially-visible images.
+ // Wait for the intersection observer to report back on the initially-visible elements.
// Note that the first callback will include _all_ observed entries per .
- if ( breadcrumbedImages.length > 0 ) {
+ if ( breadcrumbedOptimizableElements.length > 0 ) {
await new Promise( ( resolve ) => {
- imageObserver = new IntersectionObserver(
+ intersectionObserver = new IntersectionObserver(
( entries ) => {
for ( const entry of entries ) {
if ( entry.isIntersecting ) {
- imageIntersections.push( entry );
+ elementIntersections.push( entry );
}
}
resolve();
@@ -168,18 +183,18 @@ export default async function detect(
}
);
- for ( const breadcrumbedImage of breadcrumbedImages ) {
+ for ( const breadcrumbedElement of breadcrumbedOptimizableElements ) {
if (
! adminBar ||
- ! adminBar.contains( breadcrumbedImage.element )
+ ! adminBar.contains( breadcrumbedElement.element )
) {
- imageObserver.observe( breadcrumbedImage.element );
+ intersectionObserver.observe( breadcrumbedElement.element );
}
}
} );
- // Stop observing images as soon as the page scrolls since we only want initial-viewport images.
- win.addEventListener( 'scroll', disconnectImageObserver, {
+ // Stop observing as soon as the page scrolls since we only want initial-viewport elements.
+ win.addEventListener( 'scroll', disconnectIntersectionObserver, {
once: true,
passive: true,
} );
@@ -210,7 +225,7 @@ export default async function detect(
);
} );
- // Wait until the images on the page have fully loaded.
+ // Wait until the resources on the page have fully loaded.
await new Promise( ( resolve ) => {
if ( doc.readyState === 'complete' ) {
resolve();
@@ -220,19 +235,19 @@ export default async function detect(
} );
// Stop observing.
- disconnectImageObserver();
+ disconnectIntersectionObserver();
if ( isDebug ) {
log( 'Detection is stopping.' );
}
const lcpMetric = lcpMetricCandidates.at( -1 );
- for ( const imageIntersection of imageIntersections ) {
+ for ( const elementIntersection of elementIntersections ) {
log(
- 'imageIntersection.target',
- imageIntersection.target,
- getBreadcrumbs( imageIntersection.target ),
+ 'elementIntersection.target',
+ elementIntersection.target,
+ getBreadcrumbs( elementIntersection.target ),
lcpMetric &&
- imageIntersection.target ===
+ elementIntersection.target ===
lcpMetric.attribution.lcpEntry.element
? 'is LCP'
: 'is NOT LCP'
From ad2e46c058fe5a409c2d18a9a92b06e3c3d6cec1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 14:33:44 -0700
Subject: [PATCH 018/371] Leverage Map
---
.../image-loading-optimization/detect.js | 44 ++++++++++++++-----
1 file changed, 33 insertions(+), 11 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 07c8efb7c4..7754e23eb3 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -129,17 +129,25 @@ export default async function detect(
)
);
- const breadcrumbedOptimizableElements = [
+ // Create a mapping of element to
+ /** @type {Map} */
+ const breadcrumbedElementsMap = new Map();
+ for ( const breadcrumbedElement of [
...breadcrumbedImages,
...breadcrumbedElementsWithBackgrounds,
- ];
+ ] ) {
+ breadcrumbedElementsMap.set(
+ breadcrumbedElement.element,
+ breadcrumbedElement.breadcrumbs
+ );
+ }
const results = {
viewport: {
width: win.innerWidth,
height: win.innerHeight,
},
- images: [],
+ elements: [],
};
// Ensure the DOM is loaded (although it surely already is since we're executing in a module).
@@ -166,7 +174,7 @@ export default async function detect(
// Wait for the intersection observer to report back on the initially-visible elements.
// Note that the first callback will include _all_ observed entries per .
- if ( breadcrumbedOptimizableElements.length > 0 ) {
+ if ( breadcrumbedElementsMap.size > 0 ) {
await new Promise( ( resolve ) => {
intersectionObserver = new IntersectionObserver(
( entries ) => {
@@ -183,12 +191,9 @@ export default async function detect(
}
);
- for ( const breadcrumbedElement of breadcrumbedOptimizableElements ) {
- if (
- ! adminBar ||
- ! adminBar.contains( breadcrumbedElement.element )
- ) {
- intersectionObserver.observe( breadcrumbedElement.element );
+ for ( const element of breadcrumbedElementsMap.keys() ) {
+ if ( ! adminBar || ! adminBar.contains( element ) ) {
+ intersectionObserver.observe( element );
}
}
} );
@@ -242,10 +247,22 @@ export default async function detect(
const lcpMetric = lcpMetricCandidates.at( -1 );
for ( const elementIntersection of elementIntersections ) {
+ // const elementInfo = {
+ // ...
+ // };
+
+ const breadcrumbs = breadcrumbedElementsMap.get(
+ elementIntersection.target
+ );
+ if ( ! breadcrumbs ) {
+ warn( 'Unable to look up breadcrumbs for element' );
+ continue;
+ }
+
log(
'elementIntersection.target',
elementIntersection.target,
- getBreadcrumbs( elementIntersection.target ),
+ breadcrumbs,
lcpMetric &&
elementIntersection.target ===
lcpMetric.attribution.lcpEntry.element
@@ -258,4 +275,9 @@ export default async function detect(
// TODO: Send data to server.
log( results );
+
+ // Clean up.
+ breadcrumbedElementsMap.clear();
+ breadcrumbedElementsWithBackgrounds.length = 0;
+ breadcrumbedImages.length = 0;
}
From 9a50aac83dd3ec0a3626765c815fc94d844750cf Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 15:08:22 -0700
Subject: [PATCH 019/371] Assemble page metrics to send; remove unneeded
attribution build
---
.../image-loading-optimization/detect.js | 82 ++++++++++++-------
1 file changed, 54 insertions(+), 28 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 7754e23eb3..03c5d0678f 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -1,4 +1,4 @@
-/** @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution */
+/** @typedef {import("web-vitals").LCPMetric} LCPMetric */
const consoleLogPrefix = '[Image Loading Optimization]';
@@ -25,9 +25,29 @@ function warn( ...message ) {
* @property {Breadcrumb[]} breadcrumbs - Breadcrumb for the element.
*/
+/**
+ * @typedef {Object} ElementMetrics
+ * @property {boolean} isLCP - Whether it is the LCP candidate.
+ * @property {boolean} isLCPCandidate - Whether it is among the LCP candidates.
+ * @property {Breadcrumb[]} breadcrumbs - Breadcrumbs.
+ * @property {number} intersectionRatio - Intersection ratio.
+ * @property {DOMRectReadOnly} intersectionRect - Intersection rectangle.
+ * @property {DOMRectReadOnly} boundingClientRect - Bounding client rectangle.
+ */
+
+/**
+ * @typedef {Object} PageMetrics
+ * @property {Object} viewport - Viewport.
+ * @property {number} viewport.width - Viewport width.
+ * @property {number} viewport.height - Viewport height.
+ * @property {ElementMetrics[]} elements - Metrics for the elements observed on the page.
+ */
+
/**
* Get breadcrumbed elements.
*
+ * @todo We probably don't need this.
+ *
* @param {HTMLCollection|Element[]} elements Elements.
* @return {ElementBreadcrumbs[]} Breadcrumbed elements.
*/
@@ -142,14 +162,6 @@ export default async function detect(
);
}
- const results = {
- viewport: {
- width: win.innerWidth,
- height: win.innerHeight,
- },
- elements: [],
- };
-
// Ensure the DOM is loaded (although it surely already is since we're executing in a module).
await new Promise( ( resolve ) => {
if ( doc.readyState !== 'loading' ) {
@@ -208,10 +220,10 @@ export default async function detect(
// TODO: Use a local copy of web-vitals.
const { onLCP } = await import(
// eslint-disable-next-line import/no-unresolved
- 'https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module'
+ 'https://unpkg.com/web-vitals@3/dist/web-vitals.js?module'
);
- /** @type {LCPMetricWithAttribution[]} */
+ /** @type {LCPMetric[]} */
const lcpMetricCandidates = [];
// Obtain at least one LCP candidate. More may be reported before the page finishes loading.
@@ -245,36 +257,50 @@ export default async function detect(
log( 'Detection is stopping.' );
}
+ /** @type {PageMetrics} */
+ const pageMetrics = {
+ viewport: {
+ width: win.innerWidth,
+ height: win.innerHeight,
+ },
+ elements: [],
+ };
+
const lcpMetric = lcpMetricCandidates.at( -1 );
- for ( const elementIntersection of elementIntersections ) {
- // const elementInfo = {
- // ...
- // };
+ for ( const elementIntersection of elementIntersections ) {
const breadcrumbs = breadcrumbedElementsMap.get(
elementIntersection.target
);
if ( ! breadcrumbs ) {
- warn( 'Unable to look up breadcrumbs for element' );
+ if ( isDebug ) {
+ warn( 'Unable to look up breadcrumbs for element' );
+ }
continue;
}
- log(
- 'elementIntersection.target',
- elementIntersection.target,
+ const isLCP =
+ elementIntersection.target === lcpMetric?.entries[ 0 ]?.element;
+
+ /** @type {ElementMetrics} */
+ const elementMetrics = {
+ isLCP,
+ isLCPCandidate: !! lcpMetricCandidates.find(
+ ( lcpMetricCandidate ) =>
+ lcpMetricCandidate.entries[ 0 ]?.element ===
+ elementIntersection.target
+ ),
breadcrumbs,
- lcpMetric &&
- elementIntersection.target ===
- lcpMetric.attribution.lcpEntry.element
- ? 'is LCP'
- : 'is NOT LCP'
- );
- }
+ intersectionRatio: elementIntersection.intersectionRatio,
+ intersectionRect: elementIntersection.intersectionRect,
+ boundingClientRect: elementIntersection.boundingClientRect,
+ };
- log( 'lcpCandidates', lcpMetricCandidates );
+ pageMetrics.elements.push( elementMetrics );
+ }
// TODO: Send data to server.
- log( results );
+ log( pageMetrics );
// Clean up.
breadcrumbedElementsMap.clear();
From a5eaf3ac3bc4ccc6331268b89f23500fb2f59256 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 15:19:10 -0700
Subject: [PATCH 020/371] Remove unused getElementBreadcrumbsMap
---
.../image-loading-optimization/detect.js | 59 ++++---------------
1 file changed, 10 insertions(+), 49 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 03c5d0678f..7b93d1b1bf 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -19,12 +19,6 @@ function warn( ...message ) {
* @property {string} tagName - Tag name.
*/
-/**
- * @typedef {Object} ElementBreadcrumbs
- * @property {Element} element - Element node.
- * @property {Breadcrumb[]} breadcrumbs - Breadcrumb for the element.
- */
-
/**
* @typedef {Object} ElementMetrics
* @property {boolean} isLCP - Whether it is the LCP candidate.
@@ -43,29 +37,6 @@ function warn( ...message ) {
* @property {ElementMetrics[]} elements - Metrics for the elements observed on the page.
*/
-/**
- * Get breadcrumbed elements.
- *
- * @todo We probably don't need this.
- *
- * @param {HTMLCollection|Element[]} elements Elements.
- * @return {ElementBreadcrumbs[]} Breadcrumbed elements.
- */
-function getBreadcrumbedElements( elements ) {
- /** @type {ElementBreadcrumbs[]} */
- const breadcrumbedElements = [];
-
- /** @type {HTMLCollection} */
- for ( const element of elements ) {
- breadcrumbedElements.push( {
- element,
- breadcrumbs: getBreadcrumbs( element ),
- } );
- }
-
- return breadcrumbedElements;
-}
-
/**
* Gets breadcrumbs for a given element.
*
@@ -136,31 +107,21 @@ export default async function detect(
// We need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
// mutating the DOM from the original HTML rendered by the server, in which case the breadcrumbs obtained from the
// client will no longer be valid on the server. As such, the results are stored in an array and not any live list.
- const breadcrumbedImages = getBreadcrumbedElements(
- doc.body.querySelectorAll( 'img' )
- );
+ const breadcrumbedImages = doc.body.querySelectorAll( 'img' );
// We do the same for elements with background images which are not data: URLs.
- const breadcrumbedElementsWithBackgrounds = getBreadcrumbedElements(
- Array.from(
- doc.body.querySelectorAll( '[style*="background"]' )
- ).filter( ( /** @type {Element} */ el ) =>
- /url\(\s*['"](?!=data:)/.test( el.style.backgroundImage )
- )
+ const breadcrumbedElementsWithBackgrounds = Array.from(
+ doc.body.querySelectorAll( '[style*="background"]' )
+ ).filter( ( /** @type {Element} */ el ) =>
+ /url\(\s*['"](?!=data:)/.test( el.style.backgroundImage )
);
- // Create a mapping of element to
/** @type {Map} */
- const breadcrumbedElementsMap = new Map();
- for ( const breadcrumbedElement of [
- ...breadcrumbedImages,
- ...breadcrumbedElementsWithBackgrounds,
- ] ) {
- breadcrumbedElementsMap.set(
- breadcrumbedElement.element,
- breadcrumbedElement.breadcrumbs
- );
- }
+ const breadcrumbedElementsMap = new Map(
+ [ ...breadcrumbedImages, ...breadcrumbedElementsWithBackgrounds ].map(
+ ( element ) => [ element, getBreadcrumbs( element ) ]
+ )
+ );
// Ensure the DOM is loaded (although it surely already is since we're executing in a module).
await new Promise( ( resolve ) => {
From 0fcbc69c889101b336cb3ea0965a059cc4eba764 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 15:20:53 -0700
Subject: [PATCH 021/371] Remove obsolete cleanup code
---
modules/images/image-loading-optimization/detect.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 7b93d1b1bf..bd20d2f753 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -265,6 +265,4 @@ export default async function detect(
// Clean up.
breadcrumbedElementsMap.clear();
- breadcrumbedElementsWithBackgrounds.length = 0;
- breadcrumbedImages.length = 0;
}
From c1e6d3523a9de1763c29df200972cbf51fb3fd53 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 15:41:05 -0700
Subject: [PATCH 022/371] Move logic into getElementIndex helper function
---
.../image-loading-optimization/detect.js | 33 ++++++++++++-------
1 file changed, 22 insertions(+), 11 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index bd20d2f753..5e6d3d9cb3 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -1,10 +1,10 @@
/** @typedef {import("web-vitals").LCPMetric} LCPMetric */
-const consoleLogPrefix = '[Image Loading Optimization]';
-
const win = window;
const doc = win.document;
+const consoleLogPrefix = '[Image Loading Optimization]';
+
function log( ...message ) {
console.log( consoleLogPrefix, ...message );
}
@@ -37,25 +37,36 @@ function warn( ...message ) {
* @property {ElementMetrics[]} elements - Metrics for the elements observed on the page.
*/
+/**
+ * Gets element index among siblings.
+ *
+ * @param {Element} element Element.
+ * @return {number} Index.
+ */
+function getElementIndex( element ) {
+ if ( ! element.parentElement ) {
+ return 0;
+ }
+ return [ ...element.parentElement.children ].indexOf( element );
+}
+
/**
* Gets breadcrumbs for a given element.
*
- * @param {Element} element
+ * @param {Element} leafElement
* @return {Breadcrumb[]} Breadcrumbs.
*/
-function getBreadcrumbs( element ) {
+function getBreadcrumbs( leafElement ) {
/** @type {Breadcrumb[]} */
const breadcrumbs = [];
- let node = element;
- while ( node instanceof Element ) {
+ let element = leafElement;
+ while ( element instanceof Element ) {
breadcrumbs.unshift( {
- tagName: node.tagName,
- index: node.parentElement
- ? Array.from( node.parentElement.children ).indexOf( node )
- : 0,
+ tagName: element.tagName,
+ index: getElementIndex( element ),
} );
- node = node.parentElement;
+ element = element.parentElement;
}
return breadcrumbs;
From 3871670b40ab99cdb194c6b699dd765e11d67667 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 15:45:06 -0700
Subject: [PATCH 023/371] Use 3rd person singular for jsdoc
---
modules/images/image-loading-optimization/detect.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 5e6d3d9cb3..c279a7ad93 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -73,7 +73,7 @@ function getBreadcrumbs( leafElement ) {
}
/**
- * Detect the LCP element, loaded images, client viewport and store for future optimizations.
+ * Detects the LCP element, loaded images, client viewport and store for future optimizations.
*
* @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
* @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
From 68a01be56d4b3e25f3c2a5e9f692543752d9a3f1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 1 Nov 2023 17:06:00 -0700
Subject: [PATCH 024/371] Add initial REST API endpoint for storing page
metrics
---
.../image-loading-optimization/detect.js | 17 ++-
.../image-loading-optimization/hooks.php | 8 +-
.../image-loading-optimization/load.php | 1 +
.../image-loading-optimization/rest-api.php | 115 ++++++++++++++++++
4 files changed, 139 insertions(+), 2 deletions(-)
create mode 100644 modules/images/image-loading-optimization/rest-api.php
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index c279a7ad93..a0e7a3ce8c 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -78,11 +78,15 @@ function getBreadcrumbs( leafElement ) {
* @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
* @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
* @param {boolean} isDebug Whether to show debug messages.
+ * @param {string} restApiEndpoint URL for where to send the detection data.
+ * @param {string} restApiNonce Nonce for writing to the REST API.
*/
export default async function detect(
serveTime,
detectionTimeWindow,
- isDebug
+ isDebug,
+ restApiEndpoint,
+ restApiNonce
) {
const runTime = new Date().valueOf();
@@ -271,6 +275,17 @@ export default async function detect(
pageMetrics.elements.push( elementMetrics );
}
+ // TODO: Wait until idle.
+ const response = await fetch( restApiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': restApiNonce,
+ },
+ body: JSON.stringify( pageMetrics ),
+ } );
+ log( 'response:', await response.json() );
+
// TODO: Send data to server.
log( pageMetrics );
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 09692aa828..620c780a03 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -68,7 +68,13 @@ function image_loading_optimization_print_detection_script() {
*/
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
- $detect_args = array( $serve_time, $detection_time_window, WP_DEBUG );
+ $detect_args = array(
+ $serve_time,
+ $detection_time_window,
+ WP_DEBUG,
+ rest_url( '/perflab/v1/image-loading-optimization/metrics-storage' ),
+ wp_create_nonce( 'wp_rest' ),
+ );
wp_print_inline_script_tag(
sprintf(
'import detect from %s; detect( ...%s )',
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index fb6e31c2bf..6f271a96d2 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -15,3 +15,4 @@
require_once __DIR__ . '/helper.php';
require_once __DIR__ . '/hooks.php';
+require_once __DIR__ . '/rest-api.php';
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
new file mode 100644
index 0000000000..bef2555c2c
--- /dev/null
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -0,0 +1,115 @@
+ 'object',
+ 'properties' => array(
+ 'width' => array(
+ 'type' => 'number',
+ 'minimum' => 0,
+ ),
+ 'height' => array(
+ 'type' => 'number',
+ 'minimum' => 0,
+ ),
+ // TODO: There are other properties to define if we need them: x, y, top, right, bottom, left.
+ ),
+ );
+
+ register_rest_route(
+ 'perflab/v1',
+ '/image-loading-optimization/metrics-storage',
+ array(
+ 'methods' => 'POST',
+ 'callback' => 'image_loading_optimization_handle_rest_request',
+ 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors.
+ 'args' => array(
+ 'viewport' => array(
+ 'description' => __( 'Viewport dimensions', 'performance-lab' ),
+ 'type' => 'object',
+ 'required' => true,
+ 'properties' => array(
+ 'width' => array(
+ 'type' => 'int',
+ 'minimum' => 0,
+ ),
+ 'height' => array(
+ 'type' => 'int',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'elements' => array(
+ 'description' => __( 'Element metrics', 'performance-lab' ),
+ 'type' => 'array',
+ 'items' => array(
+ // See the ElementMetrics in detect.js.
+ 'type' => 'object',
+ 'properties' => array(
+ 'isLCP' => array(
+ 'type' => 'bool',
+ ),
+ 'isLCPCandidate' => array(
+ 'type' => 'bool',
+ ),
+ 'breadcrumbs' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'tagName' => array(
+ 'type' => 'string',
+ // TODO: Pattern?
+ ),
+ 'index' => array(
+ 'type' => 'int',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ ),
+ 'intersectionRatio' => array(
+ 'type' => 'number',
+ 'minimum' => 0.0,
+ 'maximum' => 1.0,
+ ),
+ 'intersectionRect' => $dom_rect_schema,
+ 'boundingClientRect' => $dom_rect_schema,
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+}
+add_action( 'rest_api_init', 'image_loading_optimization_register_endpoint' );
+
+/**
+ * Handle REST API request to store metrics.
+ *
+ * @param WP_REST_Request $request Request.
+ * @return WP_REST_Response Response.
+ */
+function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) {
+
+ return new WP_REST_Response(
+ array(
+ 'success' => true,
+ 'body' => $request->get_json_params(),
+ )
+ );
+}
From 73dbfd1675d114f8989ef6e41ce1a38f914278f6 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 11:44:49 -0700
Subject: [PATCH 025/371] Remove accounting for /wp-* symlinks in local dev
---
.eslintrc.js | 3 +--
.gitignore | 3 ---
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 66fac527a8..3627d09917 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -16,8 +16,7 @@ const config = {
globals: {
scheduler: false,
},
- // Note: The '/wp-*' pattern is to ignore symlinks which may be added for local development.
- ignorePatterns: [ '/vendor', '/node_modules', '/wp-*' ],
+ ignorePatterns: [ '/vendor', '/node_modules' ],
};
module.exports = config;
diff --git a/.gitignore b/.gitignore
index 6d54c0c132..1889d02de2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,3 @@ temp/
._*
.Trashes
.svn
-
-# Possible symlinks to wp-env install-path directories.
-/wp-*
From 66284bb3cd5fbab0fc402ed20a82d43aed5d00cb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 13:45:16 -0700
Subject: [PATCH 026/371] Add IMAGE_LOADING_OPTIMIZATION_VERSION constant
---
modules/images/image-loading-optimization/load.php | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index fb6e31c2bf..1798dcc440 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -8,6 +8,13 @@
* @since n.e.x.t
*/
+// Define the constant.
+if ( defined( 'IMAGE_LOADING_OPTIMIZATION_VERSION' ) ) {
+ return;
+}
+
+define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
+
// Do not load the code if it is already loaded through another means.
if ( function_exists( 'image_loading_optimization_buffer_output' ) ) {
return;
From b4e29d609647c8e23fc4db6532e50ba8692e82c5 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 16:06:26 -0700
Subject: [PATCH 027/371] Include url in PageMetrics and further define schema
---
.../image-loading-optimization/detect.js | 2 ++
.../image-loading-optimization/hooks.php | 3 ++
.../image-loading-optimization/rest-api.php | 34 ++++++++++++-------
3 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index a0e7a3ce8c..129079b4a7 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -31,6 +31,7 @@ function warn( ...message ) {
/**
* @typedef {Object} PageMetrics
+ * @property {string} url - URL of the page.
* @property {Object} viewport - Viewport.
* @property {number} viewport.width - Viewport width.
* @property {number} viewport.height - Viewport height.
@@ -235,6 +236,7 @@ export default async function detect(
/** @type {PageMetrics} */
const pageMetrics = {
+ url: win.location.href, // TODO: Consider sending canonical URL instead.
viewport: {
width: win.innerWidth,
height: win.innerHeight,
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 620c780a03..666c795214 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -49,6 +49,9 @@ static function ( $output ) {
/**
* Prints the script for detecting loaded images and the LCP element.
+ *
+ * @todo This should eventually only print the script if metrics are needed.
+ * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
function image_loading_optimization_print_detection_script() {
$serve_time = ceil( microtime( true ) * 1000 );
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index bef2555c2c..69519ec562 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -38,18 +38,25 @@ function image_loading_optimization_register_endpoint() {
'callback' => 'image_loading_optimization_handle_rest_request',
'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors.
'args' => array(
+ 'url' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'format' => 'uri',
+ ),
'viewport' => array(
'description' => __( 'Viewport dimensions', 'performance-lab' ),
'type' => 'object',
'required' => true,
'properties' => array(
'width' => array(
- 'type' => 'int',
- 'minimum' => 0,
+ 'type' => 'int',
+ 'required' => true,
+ 'minimum' => 0,
),
'height' => array(
- 'type' => 'int',
- 'minimum' => 0,
+ 'type' => 'int',
+ 'required' => true,
+ 'minimum' => 0,
),
),
),
@@ -61,19 +68,21 @@ function image_loading_optimization_register_endpoint() {
'type' => 'object',
'properties' => array(
'isLCP' => array(
- 'type' => 'bool',
+ 'type' => 'bool',
+ 'required' => true,
),
'isLCPCandidate' => array(
'type' => 'bool',
),
'breadcrumbs' => array(
- 'type' => 'array',
- 'items' => array(
+ 'type' => 'array',
+ 'required' => true,
+ 'items' => array(
'type' => 'object',
'properties' => array(
'tagName' => array(
- 'type' => 'string',
- // TODO: Pattern?
+ 'type' => 'string',
+ 'pattern' => '^[a-zA-Z0-9-]+$',
),
'index' => array(
'type' => 'int',
@@ -83,9 +92,10 @@ function image_loading_optimization_register_endpoint() {
),
),
'intersectionRatio' => array(
- 'type' => 'number',
- 'minimum' => 0.0,
- 'maximum' => 1.0,
+ 'type' => 'number',
+ 'required' => true,
+ 'minimum' => 0.0,
+ 'maximum' => 1.0,
),
'intersectionRect' => $dom_rect_schema,
'boundingClientRect' => $dom_rect_schema,
From ffcae75d01ea982722fa217c336c76be0d2d1ccc Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 16:12:06 -0700
Subject: [PATCH 028/371] Validate that the provided URL is for this site
---
.../images/image-loading-optimization/rest-api.php | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index 69519ec562..bcb764643a 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -39,9 +39,15 @@ function image_loading_optimization_register_endpoint() {
'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors.
'args' => array(
'url' => array(
- 'type' => 'string',
- 'required' => true,
- 'format' => 'uri',
+ 'type' => 'string',
+ 'required' => true,
+ 'format' => 'uri',
+ 'validate_callback' => static function ( $url ) {
+ if ( ! wp_validate_redirect( $url ) ) {
+ return new WP_Error( 'non_origin_url', __( 'URL for another site provided.', 'performance-lab' ) );
+ }
+ return true;
+ },
),
'viewport' => array(
'description' => __( 'Viewport dimensions', 'performance-lab' ),
From 1634fb9a5be0f8e22e4950c086b94e36e52f76cb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 17:18:32 -0700
Subject: [PATCH 029/371] Ensure both tagName and index are required
---
modules/images/image-loading-optimization/rest-api.php | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index bcb764643a..335a70aa05 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -87,12 +87,14 @@ function image_loading_optimization_register_endpoint() {
'type' => 'object',
'properties' => array(
'tagName' => array(
- 'type' => 'string',
- 'pattern' => '^[a-zA-Z0-9-]+$',
+ 'type' => 'string',
+ 'required' => true,
+ 'pattern' => '^[a-zA-Z0-9-]+$',
),
'index' => array(
- 'type' => 'int',
- 'minimum' => 0,
+ 'type' => 'int',
+ 'required' => true,
+ 'minimum' => 0,
),
),
),
From 534eba2d1378970b5a535739190bcc459c6f8fd6 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 2 Nov 2023 17:57:58 -0700
Subject: [PATCH 030/371] Add initial storage locking to protect against
flooding
---
.../image-loading-optimization/helper.php | 56 +++++++++++++++++++
.../image-loading-optimization/hooks.php | 5 ++
.../image-loading-optimization/rest-api.php | 16 +++++-
3 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php
index 77e6b12b10..59e47f937a 100644
--- a/modules/images/image-loading-optimization/helper.php
+++ b/modules/images/image-loading-optimization/helper.php
@@ -9,3 +9,59 @@
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
+
+/**
+ * Gets the TTL for the metrics storage lock.
+ *
+ * @return int TTL.
+ */
+function image_loading_optimization_get_metrics_storage_lock_ttl() {
+
+ /**
+ * Filters how long a given IP is locked from submitting another metrics-storage REST API request.
+ *
+ * @param int $ttl TTL.
+ */
+ return (int) apply_filters( 'perflab_image_loading_detection_lock_ttl', 10 * MINUTE_IN_SECONDS );
+}
+
+/**
+ * Gets transient key for locking metrics storage (for the current IP).
+ *
+ * @return string Transient key.
+ */
+function image_loading_optimization_get_metrics_storage_lock_transient_key() {
+ $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
+ return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
+}
+
+/**
+ * Sets metrics storage lock (for the current IP).
+ */
+function image_loading_optimization_set_metrics_storage_lock() {
+ $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
+ $key = image_loading_optimization_get_metrics_storage_lock_transient_key();
+ if ( 0 === $ttl ) {
+ delete_transient( $key );
+ } else {
+ set_transient( $key, time(), $ttl );
+ }
+}
+
+/**
+ * Checks whether metrics storage is locked (for the current IP).
+ *
+ * @todo This isn't working properly?
+ * @return bool Whether locked.
+ */
+function image_loading_optimization_is_metrics_storage_locked() {
+ $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
+ if ( 0 === $ttl ) {
+ return false;
+ }
+ $transient = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() );
+ if ( 0 === $transient ) {
+ return false;
+ }
+ return time() - $transient < $ttl;
+}
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 666c795214..88dd70635b 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -54,6 +54,11 @@ static function ( $output ) {
* @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
function image_loading_optimization_print_detection_script() {
+
+ if ( image_loading_optimization_is_metrics_storage_locked() ) {
+ return;
+ }
+
$serve_time = ceil( microtime( true ) * 1000 );
/**
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index 335a70aa05..0746ff1315 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -36,7 +36,17 @@ function image_loading_optimization_register_endpoint() {
array(
'methods' => 'POST',
'callback' => 'image_loading_optimization_handle_rest_request',
- 'permission_callback' => '__return_true', // Needs to be available to unauthenticated visitors.
+ 'permission_callback' => static function () {
+ // Needs to be available to unauthenticated visitors.
+ if ( image_loading_optimization_is_metrics_storage_locked() ) {
+ return new WP_Error(
+ 'metrics_storage_locked',
+ __( 'Metrics storage is presently locked for the current IP.', 'performance-lab' ),
+ array( 'status' => 403 )
+ );
+ }
+ return true;
+ },
'args' => array(
'url' => array(
'type' => 'string',
@@ -124,6 +134,10 @@ function image_loading_optimization_register_endpoint() {
*/
function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) {
+ // TODO: We need storage.
+
+ image_loading_optimization_set_metrics_storage_lock();
+
return new WP_REST_Response(
array(
'success' => true,
From ba30471f82bc97fae8eb9788375d81d2d2bdabaa Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 3 Nov 2023 11:44:18 -0700
Subject: [PATCH 031/371] Update metrics storage locking
---
modules/images/image-loading-optimization/helper.php | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php
index 59e47f937a..e55f0bb992 100644
--- a/modules/images/image-loading-optimization/helper.php
+++ b/modules/images/image-loading-optimization/helper.php
@@ -20,14 +20,17 @@ function image_loading_optimization_get_metrics_storage_lock_ttl() {
/**
* Filters how long a given IP is locked from submitting another metrics-storage REST API request.
*
+ * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development.
+ *
* @param int $ttl TTL.
*/
- return (int) apply_filters( 'perflab_image_loading_detection_lock_ttl', 10 * MINUTE_IN_SECONDS );
+ return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
}
/**
* Gets transient key for locking metrics storage (for the current IP).
*
+ * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
* @return string Transient key.
*/
function image_loading_optimization_get_metrics_storage_lock_transient_key() {
@@ -51,7 +54,6 @@ function image_loading_optimization_set_metrics_storage_lock() {
/**
* Checks whether metrics storage is locked (for the current IP).
*
- * @todo This isn't working properly?
* @return bool Whether locked.
*/
function image_loading_optimization_is_metrics_storage_locked() {
@@ -59,9 +61,9 @@ function image_loading_optimization_is_metrics_storage_locked() {
if ( 0 === $ttl ) {
return false;
}
- $transient = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() );
- if ( 0 === $transient ) {
+ $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() );
+ if ( 0 === $locked_time ) {
return false;
}
- return time() - $transient < $ttl;
+ return time() - $locked_time < $ttl;
}
From 4e0d7de6cc43c0d7bd9e89c4352b754239fcda79 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 3 Nov 2023 11:54:32 -0700
Subject: [PATCH 032/371] Move storage helper functions into storage.php
---
.../image-loading-optimization/helper.php | 58 ----------------
.../image-loading-optimization/load.php | 1 +
.../image-loading-optimization/storage.php | 69 +++++++++++++++++++
3 files changed, 70 insertions(+), 58 deletions(-)
create mode 100644 modules/images/image-loading-optimization/storage.php
diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php
index e55f0bb992..77e6b12b10 100644
--- a/modules/images/image-loading-optimization/helper.php
+++ b/modules/images/image-loading-optimization/helper.php
@@ -9,61 +9,3 @@
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
-
-/**
- * Gets the TTL for the metrics storage lock.
- *
- * @return int TTL.
- */
-function image_loading_optimization_get_metrics_storage_lock_ttl() {
-
- /**
- * Filters how long a given IP is locked from submitting another metrics-storage REST API request.
- *
- * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development.
- *
- * @param int $ttl TTL.
- */
- return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
-}
-
-/**
- * Gets transient key for locking metrics storage (for the current IP).
- *
- * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
- * @return string Transient key.
- */
-function image_loading_optimization_get_metrics_storage_lock_transient_key() {
- $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
- return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
-}
-
-/**
- * Sets metrics storage lock (for the current IP).
- */
-function image_loading_optimization_set_metrics_storage_lock() {
- $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
- $key = image_loading_optimization_get_metrics_storage_lock_transient_key();
- if ( 0 === $ttl ) {
- delete_transient( $key );
- } else {
- set_transient( $key, time(), $ttl );
- }
-}
-
-/**
- * Checks whether metrics storage is locked (for the current IP).
- *
- * @return bool Whether locked.
- */
-function image_loading_optimization_is_metrics_storage_locked() {
- $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
- if ( 0 === $ttl ) {
- return false;
- }
- $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() );
- if ( 0 === $locked_time ) {
- return false;
- }
- return time() - $locked_time < $ttl;
-}
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 6f271a96d2..e760b0a6e9 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -15,4 +15,5 @@
require_once __DIR__ . '/helper.php';
require_once __DIR__ . '/hooks.php';
+require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/rest-api.php';
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
new file mode 100644
index 0000000000..e3032c28bb
--- /dev/null
+++ b/modules/images/image-loading-optimization/storage.php
@@ -0,0 +1,69 @@
+
Date: Fri, 3 Nov 2023 12:15:23 -0700
Subject: [PATCH 033/371] Add post type for page metrics storage
---
.../image-loading-optimization/storage.php | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index e3032c28bb..49a33ccd75 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -10,6 +10,8 @@
exit; // Exit if accessed directly.
}
+define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
+
/**
* Gets the TTL for the metrics storage lock.
*
@@ -67,3 +69,28 @@ function image_loading_optimization_is_metrics_storage_locked() {
}
return time() - $locked_time < $ttl;
}
+
+/**
+ * Register post type for metrics storage.
+ *
+ * This the configuration for this post type is similar to the oembed_cache in core.
+ */
+function image_loading_optimization_register_page_metrics_post_type() {
+ register_post_type(
+ IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ array(
+ 'labels' => array(
+ 'name' => __( 'Page Metrics', 'performance-lab' ),
+ 'singular_name' => __( 'Page Metrics', 'performance-lab' ),
+ ),
+ 'public' => false,
+ 'hierarchical' => false,
+ 'rewrite' => false,
+ 'query_var' => false,
+ 'delete_with_user' => false,
+ 'can_export' => false,
+ 'supports' => array(),
+ )
+ );
+}
+add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' );
From b7396c8106c5d224bc1853aa14a4a0144ac0da4e Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 17:15:32 -0800
Subject: [PATCH 034/371] WIP
---
composer.json | 3 +-
.../image-loading-optimization/rest-api.php | 2 +-
.../image-loading-optimization/storage.php | 189 +++++++++++++++++-
3 files changed, 191 insertions(+), 3 deletions(-)
diff --git a/composer.json b/composer.json
index 8ddca6af09..27cd7d9afc 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,8 @@
},
"require": {
"composer/installers": "~1.0",
- "php": ">=7|^8"
+ "php": ">=7|^8",
+ "ext-json": "*"
},
"scripts": {
"phpstan": "phpstan analyze --ansi --memory-limit=2048M",
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index 0746ff1315..a347f75d29 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -32,7 +32,7 @@ function image_loading_optimization_register_endpoint() {
register_rest_route(
'perflab/v1',
- '/image-loading-optimization/metrics-storage',
+ '/image-loading-optimization/metrics-storage', // @todo or rather metric-storage?
array(
'methods' => 'POST',
'callback' => 'image_loading_optimization_handle_rest_request',
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index 49a33ccd75..c9413609b3 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -29,6 +29,22 @@ function image_loading_optimization_get_metrics_storage_lock_ttl() {
return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
}
+/**
+ * Gets the maximum width for a viewport to be considered as a mobile device.
+ *
+ * @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops.
+ * @return int Viewport width.
+ */
+function image_loading_optimization_get_max_mobile_viewport_width() {
+
+ /**
+ * Filters the maximum width for a viewport to be considered as a mobile device.
+ *
+ * @param int $mobile_max_width Mobile max width.
+ */
+ return (int) apply_filters( 'perflab_image_loading_optimization_max_mobile_viewport_with', 480 );
+}
+
/**
* Gets transient key for locking metrics storage (for the current IP).
*
@@ -89,8 +105,179 @@ function image_loading_optimization_register_page_metrics_post_type() {
'query_var' => false,
'delete_with_user' => false,
'can_export' => false,
- 'supports' => array(),
+ 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name.
)
);
}
add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' );
+
+/**
+ * Gets desired sample size for a viewport's page metrics.
+ *
+ * @return int
+ */
+function image_loading_optimization_get_page_metrics_viewport_sample_size() {
+ /**
+ * Filters desired sample size for a viewport's page metrics.
+ *
+ * @param int $sample_size Sample size.
+ */
+ return (int) apply_filters( 'perflab_image_loading_optimization_page_metrics_viewport_sample_size', 10 );
+}
+
+/**
+ * Get slug for page metrics post.
+ *
+ * @param string $url URL.
+ * @return string Slug for URL.
+ */
+function image_loading_optimization_get_page_metrics_slug( $url ) {
+ return md5( $url );
+}
+
+/**
+ * Get page metrics post.
+ *
+ * @param string $url URL.
+ * @return WP_Post|null Post object if exists.
+ */
+function image_loading_optimization_get_page_metrics_post( $url ) {
+ $post_query = new WP_Query(
+ array(
+ 'post_type' => IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ 'post_status' => 'publish',
+ 'name' => image_loading_optimization_get_page_metrics_slug( $url ),
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'cache_results' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ 'lazy_load_term_meta' => false,
+ )
+ );
+
+ $post = array_shift( $post_query->posts );
+ if ( $post instanceof WP_Post ) {
+ return $post;
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Store page metrics.
+ *
+ * @param WP_Post $post Page metrics post.
+ * @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
+ */
+function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
+ $page_metrics = json_decode( $post->post_content, true );
+ if ( json_last_error() ) {
+ return new WP_Error(
+ 'page_metrics_json_parse_error',
+ sprintf(
+ /* translators: 1: Post type slug, 2: JSON error message */
+ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
+ IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ json_last_error_msg()
+ )
+ );
+ }
+ if ( ! is_array( $page_metrics ) ) {
+ return new WP_Error(
+ 'page_metrics_invalid_data_format',
+ sprintf(
+ /* translators: %s is post type slug */
+ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
+ IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE
+ )
+ );
+ }
+ return $page_metrics;
+}
+
+/**
+ *
+ * @todo This needs to take a set of page metrics and segment the individual metrics into breakpoints.
+ *
+ * @return void
+ */
+function image_loading_optimization_segment_stored_page_metrics() {
+
+}
+
+/**
+ * Store page metrics.
+ *
+ * The $validated_page_metrics parameter has the following array shape:
+ *
+ * {
+ * 'url': string,
+ * 'viewport': array{
+ * 'width': int,
+ * 'height': int
+ * },
+ * 'elements': array
+ * }
+ *
+ * @param array $validated_page_metrics Page metrics, already validated by REST API.
+ * @return true|WP_Error True on success or WP_Error otherwise.
+ */
+function image_loading_optimization_store_page_metrics( array $validated_page_metrics ) {
+ $url = $validated_page_metrics['url'];
+ unset( $validated_page_metrics['url'] ); // Not stored in post_content but rather in post_title/post_name.
+
+ // TODO: What about storing a version identifier?
+ $post_data = array(
+ 'post_title' => $url,
+ );
+
+ $post = image_loading_optimization_get_page_metrics_post( $url );
+
+ if ( $post instanceof WP_Post ) {
+ $post_data['ID'] = $post->ID;
+ $post_data['post_name'] = $post->post_name;
+
+ $page_metrics = image_loading_optimization_parse_stored_page_metrics( $post );
+ if ( $page_metrics instanceof WP_Error ) {
+ if ( function_exists( 'wp_trigger_error' ) ) {
+ wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) );
+ }
+ $page_metrics = array();
+ }
+ } else {
+ $post_data['post_name'] = image_loading_optimization_get_page_metrics_slug( $url );
+ $page_metrics = array();
+ }
+
+ // TODO: Unshift the first metrics entry if we are currently at the max allowed.
+ $segmented_page_metrics =
+
+ $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width();
+ $viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size();
+
+ $viewport_page_metrics = array();
+
+ $existing_storage[] = $validated_page_metrics;
+
+
+ $post_data['post_content'] = wp_json_encode( $validated_page_metrics );
+
+ $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
+ if ( $has_kses ) {
+ // Prevent KSES from corrupting JSON in post_content.
+ kses_remove_filters();
+ }
+
+ if ( isset( $post_data['ID'] ) ) {
+ $result = wp_update_post( wp_slash( $post_data ), true );
+ } else {
+ $result = wp_insert_post( wp_slash( $post_data ), true );
+ }
+
+ if ( $has_kses ) {
+ kses_init_filters();
+ }
+
+ return $result instanceof WP_Error ? $result : true;
+}
From 912abef650c67866fe14ef2bd3d782d320de9adc Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 17:18:39 -0800
Subject: [PATCH 035/371] Undo turning off no-console eslint rule
---
.eslintrc.js | 1 -
modules/images/image-loading-optimization/detect.js | 12 ++++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 3627d09917..4a43261aee 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -8,7 +8,6 @@ const config = {
rules: {
...( wpConfig?.rules || {} ),
'jsdoc/valid-types': 'off',
- 'no-console': 'off',
},
env: {
browser: true,
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index c279a7ad93..f6f0677a09 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -5,11 +5,23 @@ const doc = win.document;
const consoleLogPrefix = '[Image Loading Optimization]';
+/**
+ * Log a message.
+ *
+ * @param {...*} message
+ */
function log( ...message ) {
+ // eslint-disable-next-line no-console
console.log( consoleLogPrefix, ...message );
}
+/**
+ * Log a warning.
+ *
+ * @param {...*} message
+ */
function warn( ...message ) {
+ // eslint-disable-next-line no-console
console.warn( consoleLogPrefix, ...message );
}
From 01115e5f749dd34a74fdb13f24cd551b92915ff2 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 17:27:06 -0800
Subject: [PATCH 036/371] Ignore webp-uploads/fallback.js from linting for now
---
.eslintrc.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 4a43261aee..d2607fb45e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -15,7 +15,11 @@ const config = {
globals: {
scheduler: false,
},
- ignorePatterns: [ '/vendor', '/node_modules' ],
+ ignorePatterns: [
+ '/vendor',
+ '/node_modules',
+ '/modules/images/webp-uploads/fallback.js', // TODO: Issues need to be fixed here.
+ ],
};
module.exports = config;
From 0e5b79f660970bfa1d4dffdf2bcdebfb0099bb3f Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 19:53:06 -0800
Subject: [PATCH 037/371] Fix storage and improve naming
---
.../image-loading-optimization/hooks.php | 4 +-
.../image-loading-optimization/rest-api.php | 31 +++++---
.../image-loading-optimization/storage.php | 74 ++++++++++---------
3 files changed, 61 insertions(+), 48 deletions(-)
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 88dd70635b..43fa5a1686 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -55,7 +55,7 @@ static function ( $output ) {
*/
function image_loading_optimization_print_detection_script() {
- if ( image_loading_optimization_is_metrics_storage_locked() ) {
+ if ( image_loading_optimization_is_page_metric_storage_locked() ) {
return;
}
@@ -80,7 +80,7 @@ function image_loading_optimization_print_detection_script() {
$serve_time,
$detection_time_window,
WP_DEBUG,
- rest_url( '/perflab/v1/image-loading-optimization/metrics-storage' ),
+ rest_url( IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE . IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE ),
wp_create_nonce( 'wp_rest' ),
);
wp_print_inline_script_tag(
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index a347f75d29..6f33a8c17b 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -10,8 +10,11 @@
exit; // Exit if accessed directly.
}
+define( 'IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE', 'image-loading-optimization/v1' );
+define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' );
+
/**
- * Register endpoint for storage of metrics.
+ * Register endpoint for storage of page metric.
*/
function image_loading_optimization_register_endpoint() {
@@ -31,17 +34,17 @@ function image_loading_optimization_register_endpoint() {
);
register_rest_route(
- 'perflab/v1',
- '/image-loading-optimization/metrics-storage', // @todo or rather metric-storage?
+ IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE,
+ IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE,
array(
'methods' => 'POST',
'callback' => 'image_loading_optimization_handle_rest_request',
'permission_callback' => static function () {
// Needs to be available to unauthenticated visitors.
- if ( image_loading_optimization_is_metrics_storage_locked() ) {
+ if ( image_loading_optimization_is_page_metric_storage_locked() ) {
return new WP_Error(
- 'metrics_storage_locked',
- __( 'Metrics storage is presently locked for the current IP.', 'performance-lab' ),
+ 'page_metric_storage_locked',
+ __( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ),
array( 'status' => 403 )
);
}
@@ -130,18 +133,26 @@ function image_loading_optimization_register_endpoint() {
* Handle REST API request to store metrics.
*
* @param WP_REST_Request $request Request.
- * @return WP_REST_Response Response.
+ * @return WP_REST_Response|WP_Error Response.
*/
function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) {
// TODO: We need storage.
- image_loading_optimization_set_metrics_storage_lock();
+ image_loading_optimization_set_page_metric_storage_lock();
+
+ $result = image_loading_optimization_store_page_metric( $request->get_json_params() );
+
+ if ( $result instanceof WP_Error ) {
+ return $result;
+ }
- return new WP_REST_Response(
+ $response = new WP_REST_Response(
array(
'success' => true,
- 'body' => $request->get_json_params(),
+ 'post_id' => $result,
)
);
+ $response->set_status( 201 );
+ return $response;
}
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index c9413609b3..9f430b9dd4 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -13,16 +13,16 @@
define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
/**
- * Gets the TTL for the metrics storage lock.
+ * Gets the TTL for the page metric storage lock.
*
* @return int TTL.
*/
-function image_loading_optimization_get_metrics_storage_lock_ttl() {
+function image_loading_optimization_get_page_metric_storage_lock_ttl() {
/**
- * Filters how long a given IP is locked from submitting another metrics-storage REST API request.
+ * Filters how long a given IP is locked from submitting another metric-storage REST API request.
*
- * Filtering the TTL to zero will disable any metrics storage locking. This is useful during development.
+ * Filtering the TTL to zero will disable any metric storage locking. This is useful during development.
*
* @param int $ttl TTL.
*/
@@ -46,22 +46,22 @@ function image_loading_optimization_get_max_mobile_viewport_width() {
}
/**
- * Gets transient key for locking metrics storage (for the current IP).
+ * Gets transient key for locking page metric storage (for the current IP).
*
* @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
* @return string Transient key.
*/
-function image_loading_optimization_get_metrics_storage_lock_transient_key() {
+function image_loading_optimization_get_page_metric_storage_lock_transient_key() {
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
}
/**
- * Sets metrics storage lock (for the current IP).
+ * Sets page metric storage lock (for the current IP).
*/
-function image_loading_optimization_set_metrics_storage_lock() {
- $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
- $key = image_loading_optimization_get_metrics_storage_lock_transient_key();
+function image_loading_optimization_set_page_metric_storage_lock() {
+ $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl();
+ $key = image_loading_optimization_get_page_metric_storage_lock_transient_key();
if ( 0 === $ttl ) {
delete_transient( $key );
} else {
@@ -70,16 +70,16 @@ function image_loading_optimization_set_metrics_storage_lock() {
}
/**
- * Checks whether metrics storage is locked (for the current IP).
+ * Checks whether page metric storage is locked (for the current IP).
*
* @return bool Whether locked.
*/
-function image_loading_optimization_is_metrics_storage_locked() {
- $ttl = image_loading_optimization_get_metrics_storage_lock_ttl();
+function image_loading_optimization_is_page_metric_storage_locked() {
+ $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl();
if ( 0 === $ttl ) {
return false;
}
- $locked_time = (int) get_transient( image_loading_optimization_get_metrics_storage_lock_transient_key() );
+ $locked_time = (int) get_transient( image_loading_optimization_get_page_metric_storage_lock_transient_key() );
if ( 0 === $locked_time ) {
return false;
}
@@ -87,7 +87,7 @@ function image_loading_optimization_is_metrics_storage_locked() {
}
/**
- * Register post type for metrics storage.
+ * Register post type for page metrics storage.
*
* This the configuration for this post type is similar to the oembed_cache in core.
*/
@@ -126,7 +126,7 @@ function image_loading_optimization_get_page_metrics_viewport_sample_size() {
}
/**
- * Get slug for page metrics post.
+ * Gets slug for page metrics post.
*
* @param string $url URL.
* @return string Slug for URL.
@@ -165,7 +165,7 @@ function image_loading_optimization_get_page_metrics_post( $url ) {
}
/**
- * Store page metrics.
+ * Parses post content in page metrics post.
*
* @param WP_Post $post Page metrics post.
* @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
@@ -202,14 +202,14 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
*
* @return void
*/
-function image_loading_optimization_segment_stored_page_metrics() {
+function image_loading_optimization_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) {
}
/**
- * Store page metrics.
+ * Stores page metric by merging it with the other page metrics for a given URL.
*
- * The $validated_page_metrics parameter has the following array shape:
+ * The $validated_page_metric parameter has the following array shape:
*
* {
* 'url': string,
@@ -220,12 +220,14 @@ function image_loading_optimization_segment_stored_page_metrics() {
* 'elements': array
* }
*
- * @param array $validated_page_metrics Page metrics, already validated by REST API.
- * @return true|WP_Error True on success or WP_Error otherwise.
+ * @param array $validated_page_metric Page metric, already validated by REST API.
+ *
+ * @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function image_loading_optimization_store_page_metrics( array $validated_page_metrics ) {
- $url = $validated_page_metrics['url'];
- unset( $validated_page_metrics['url'] ); // Not stored in post_content but rather in post_title/post_name.
+function image_loading_optimization_store_page_metric( array $validated_page_metric ) {
+ $url = $validated_page_metric['url'];
+ unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name.
+ $validated_page_metric['timestamp'] = time();
// TODO: What about storing a version identifier?
$post_data = array(
@@ -250,18 +252,16 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me
$page_metrics = array();
}
- // TODO: Unshift the first metrics entry if we are currently at the max allowed.
- $segmented_page_metrics =
-
- $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width();
+ // Add the provided page metric to the page metrics.
+ // TODO: Need to implement viewport breakpoint segmenting.
+ // $segmented_page_metrics =
+ // $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width();
$viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size();
+ // $viewport_page_metrics = array();
+ $page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric.
+ array_unshift( $page_metrics, $validated_page_metric );
- $viewport_page_metrics = array();
-
- $existing_storage[] = $validated_page_metrics;
-
-
- $post_data['post_content'] = wp_json_encode( $validated_page_metrics );
+ $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
if ( $has_kses ) {
@@ -269,6 +269,8 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me
kses_remove_filters();
}
+ $post_data['post_type'] = IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE;
+ $post_data['post_status'] = 'publish';
if ( isset( $post_data['ID'] ) ) {
$result = wp_update_post( wp_slash( $post_data ), true );
} else {
@@ -279,5 +281,5 @@ function image_loading_optimization_store_page_metrics( array $validated_page_me
kses_init_filters();
}
- return $result instanceof WP_Error ? $result : true;
+ return $result;
}
From 739a53e55f461cada695d4a24e9dccfc95f6567b Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 20:01:40 -0800
Subject: [PATCH 038/371] Use ILO consistently as the prefix
---
.../image-loading-optimization/hooks.php | 12 ++--
.../image-loading-optimization/load.php | 6 +-
.../image-loading-optimization/rest-api.php | 22 +++----
.../image-loading-optimization/storage.php | 64 +++++++++----------
4 files changed, 52 insertions(+), 52 deletions(-)
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 43fa5a1686..718cfe8853 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -31,7 +31,7 @@
* @param mixed $passthrough Optional. Filter value. Default null.
* @return mixed Unmodified value of $passthrough.
*/
-function image_loading_optimization_buffer_output( $passthrough = null ) {
+function ilo_buffer_output( $passthrough = null ) {
ob_start(
static function ( $output ) {
/**
@@ -45,7 +45,7 @@ static function ( $output ) {
);
return $passthrough;
}
-add_filter( 'template_include', 'image_loading_optimization_buffer_output', PHP_INT_MAX );
+add_filter( 'template_include', 'ilo_buffer_output', PHP_INT_MAX );
/**
* Prints the script for detecting loaded images and the LCP element.
@@ -53,9 +53,9 @@ static function ( $output ) {
* @todo This should eventually only print the script if metrics are needed.
* @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
-function image_loading_optimization_print_detection_script() {
+function ilo_print_detection_script() {
- if ( image_loading_optimization_is_page_metric_storage_locked() ) {
+ if ( ilo_is_page_metric_storage_locked() ) {
return;
}
@@ -80,7 +80,7 @@ function image_loading_optimization_print_detection_script() {
$serve_time,
$detection_time_window,
WP_DEBUG,
- rest_url( IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE . IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE ),
+ rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ),
wp_create_nonce( 'wp_rest' ),
);
wp_print_inline_script_tag(
@@ -92,4 +92,4 @@ function image_loading_optimization_print_detection_script() {
array( 'type' => 'module' )
);
}
-add_action( 'wp_print_footer_scripts', 'image_loading_optimization_print_detection_script' );
+add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' );
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 0828ab47bb..2aaace8afa 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -9,14 +9,14 @@
*/
// Define the constant.
-if ( defined( 'IMAGE_LOADING_OPTIMIZATION_VERSION' ) ) {
+if ( defined( 'ILO_VERSION' ) ) {
return;
}
-define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
+define( 'ILO_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
// Do not load the code if it is already loaded through another means.
-if ( function_exists( 'image_loading_optimization_buffer_output' ) ) {
+if ( function_exists( 'ilo_buffer_output' ) ) {
return;
}
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index 6f33a8c17b..672f5cf9e8 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -10,13 +10,13 @@
exit; // Exit if accessed directly.
}
-define( 'IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE', 'image-loading-optimization/v1' );
-define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' );
+define( 'ILO_REST_API_NAMESPACE', 'image-loading-optimization/v1' );
+define( 'ILO_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' );
/**
* Register endpoint for storage of page metric.
*/
-function image_loading_optimization_register_endpoint() {
+function ilo_register_endpoint() {
$dom_rect_schema = array(
'type' => 'object',
@@ -34,14 +34,14 @@ function image_loading_optimization_register_endpoint() {
);
register_rest_route(
- IMAGE_LOADING_OPTIMIZATION_REST_API_NAMESPACE,
- IMAGE_LOADING_OPTIMIZATION_PAGE_METRIC_STORAGE_ROUTE,
+ ILO_REST_API_NAMESPACE,
+ ILO_PAGE_METRIC_STORAGE_ROUTE,
array(
'methods' => 'POST',
- 'callback' => 'image_loading_optimization_handle_rest_request',
+ 'callback' => 'ilo_handle_rest_request',
'permission_callback' => static function () {
// Needs to be available to unauthenticated visitors.
- if ( image_loading_optimization_is_page_metric_storage_locked() ) {
+ if ( ilo_is_page_metric_storage_locked() ) {
return new WP_Error(
'page_metric_storage_locked',
__( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ),
@@ -127,7 +127,7 @@ function image_loading_optimization_register_endpoint() {
)
);
}
-add_action( 'rest_api_init', 'image_loading_optimization_register_endpoint' );
+add_action( 'rest_api_init', 'ilo_register_endpoint' );
/**
* Handle REST API request to store metrics.
@@ -135,13 +135,13 @@ function image_loading_optimization_register_endpoint() {
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
-function image_loading_optimization_handle_rest_request( WP_REST_Request $request ) {
+function ilo_handle_rest_request( WP_REST_Request $request ) {
// TODO: We need storage.
- image_loading_optimization_set_page_metric_storage_lock();
+ ilo_set_page_metric_storage_lock();
- $result = image_loading_optimization_store_page_metric( $request->get_json_params() );
+ $result = ilo_store_page_metric( $request->get_json_params() );
if ( $result instanceof WP_Error ) {
return $result;
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index 9f430b9dd4..8e92fc2e01 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -10,14 +10,14 @@
exit; // Exit if accessed directly.
}
-define( 'IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
+define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
/**
* Gets the TTL for the page metric storage lock.
*
* @return int TTL.
*/
-function image_loading_optimization_get_page_metric_storage_lock_ttl() {
+function ilo_get_page_metric_storage_lock_ttl() {
/**
* Filters how long a given IP is locked from submitting another metric-storage REST API request.
@@ -26,7 +26,7 @@ function image_loading_optimization_get_page_metric_storage_lock_ttl() {
*
* @param int $ttl TTL.
*/
- return (int) apply_filters( 'perflab_image_loading_optimization_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
+ return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
}
/**
@@ -35,14 +35,14 @@ function image_loading_optimization_get_page_metric_storage_lock_ttl() {
* @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops.
* @return int Viewport width.
*/
-function image_loading_optimization_get_max_mobile_viewport_width() {
+function ilo_get_max_mobile_viewport_width() {
/**
* Filters the maximum width for a viewport to be considered as a mobile device.
*
* @param int $mobile_max_width Mobile max width.
*/
- return (int) apply_filters( 'perflab_image_loading_optimization_max_mobile_viewport_with', 480 );
+ return (int) apply_filters( 'ilo_max_mobile_viewport_with', 480 );
}
/**
@@ -51,7 +51,7 @@ function image_loading_optimization_get_max_mobile_viewport_width() {
* @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
* @return string Transient key.
*/
-function image_loading_optimization_get_page_metric_storage_lock_transient_key() {
+function ilo_get_page_metric_storage_lock_transient_key() {
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
}
@@ -59,9 +59,9 @@ function image_loading_optimization_get_page_metric_storage_lock_transient_key()
/**
* Sets page metric storage lock (for the current IP).
*/
-function image_loading_optimization_set_page_metric_storage_lock() {
- $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl();
- $key = image_loading_optimization_get_page_metric_storage_lock_transient_key();
+function ilo_set_page_metric_storage_lock() {
+ $ttl = ilo_get_page_metric_storage_lock_ttl();
+ $key = ilo_get_page_metric_storage_lock_transient_key();
if ( 0 === $ttl ) {
delete_transient( $key );
} else {
@@ -74,12 +74,12 @@ function image_loading_optimization_set_page_metric_storage_lock() {
*
* @return bool Whether locked.
*/
-function image_loading_optimization_is_page_metric_storage_locked() {
- $ttl = image_loading_optimization_get_page_metric_storage_lock_ttl();
+function ilo_is_page_metric_storage_locked() {
+ $ttl = ilo_get_page_metric_storage_lock_ttl();
if ( 0 === $ttl ) {
return false;
}
- $locked_time = (int) get_transient( image_loading_optimization_get_page_metric_storage_lock_transient_key() );
+ $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() );
if ( 0 === $locked_time ) {
return false;
}
@@ -91,9 +91,9 @@ function image_loading_optimization_is_page_metric_storage_locked() {
*
* This the configuration for this post type is similar to the oembed_cache in core.
*/
-function image_loading_optimization_register_page_metrics_post_type() {
+function ilo_register_page_metrics_post_type() {
register_post_type(
- IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ ILO_PAGE_METRICS_POST_TYPE,
array(
'labels' => array(
'name' => __( 'Page Metrics', 'performance-lab' ),
@@ -109,20 +109,20 @@ function image_loading_optimization_register_page_metrics_post_type() {
)
);
}
-add_action( 'init', 'image_loading_optimization_register_page_metrics_post_type' );
+add_action( 'init', 'ilo_register_page_metrics_post_type' );
/**
* Gets desired sample size for a viewport's page metrics.
*
* @return int
*/
-function image_loading_optimization_get_page_metrics_viewport_sample_size() {
+function ilo_get_page_metrics_viewport_sample_size() {
/**
* Filters desired sample size for a viewport's page metrics.
*
* @param int $sample_size Sample size.
*/
- return (int) apply_filters( 'perflab_image_loading_optimization_page_metrics_viewport_sample_size', 10 );
+ return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 );
}
/**
@@ -131,7 +131,7 @@ function image_loading_optimization_get_page_metrics_viewport_sample_size() {
* @param string $url URL.
* @return string Slug for URL.
*/
-function image_loading_optimization_get_page_metrics_slug( $url ) {
+function ilo_get_page_metrics_slug( $url ) {
return md5( $url );
}
@@ -141,12 +141,12 @@ function image_loading_optimization_get_page_metrics_slug( $url ) {
* @param string $url URL.
* @return WP_Post|null Post object if exists.
*/
-function image_loading_optimization_get_page_metrics_post( $url ) {
+function ilo_get_page_metrics_post( $url ) {
$post_query = new WP_Query(
array(
- 'post_type' => IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ 'post_type' => ILO_PAGE_METRICS_POST_TYPE,
'post_status' => 'publish',
- 'name' => image_loading_optimization_get_page_metrics_slug( $url ),
+ 'name' => ilo_get_page_metrics_slug( $url ),
'posts_per_page' => 1,
'no_found_rows' => true,
'cache_results' => true,
@@ -170,7 +170,7 @@ function image_loading_optimization_get_page_metrics_post( $url ) {
* @param WP_Post $post Page metrics post.
* @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
*/
-function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
+function ilo_parse_stored_page_metrics( WP_Post $post ) {
$page_metrics = json_decode( $post->post_content, true );
if ( json_last_error() ) {
return new WP_Error(
@@ -178,7 +178,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
sprintf(
/* translators: 1: Post type slug, 2: JSON error message */
__( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
- IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE,
+ ILO_PAGE_METRICS_POST_TYPE,
json_last_error_msg()
)
);
@@ -189,7 +189,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
sprintf(
/* translators: %s is post type slug */
__( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
- IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE
+ ILO_PAGE_METRICS_POST_TYPE
)
);
}
@@ -202,7 +202,7 @@ function image_loading_optimization_parse_stored_page_metrics( WP_Post $post ) {
*
* @return void
*/
-function image_loading_optimization_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) {
+function ilo_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) {
}
@@ -224,7 +224,7 @@ function image_loading_optimization_segment_stored_page_metrics( array $page_met
*
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function image_loading_optimization_store_page_metric( array $validated_page_metric ) {
+function ilo_store_page_metric( array $validated_page_metric ) {
$url = $validated_page_metric['url'];
unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name.
$validated_page_metric['timestamp'] = time();
@@ -234,13 +234,13 @@ function image_loading_optimization_store_page_metric( array $validated_page_met
'post_title' => $url,
);
- $post = image_loading_optimization_get_page_metrics_post( $url );
+ $post = ilo_get_page_metrics_post( $url );
if ( $post instanceof WP_Post ) {
$post_data['ID'] = $post->ID;
$post_data['post_name'] = $post->post_name;
- $page_metrics = image_loading_optimization_parse_stored_page_metrics( $post );
+ $page_metrics = ilo_parse_stored_page_metrics( $post );
if ( $page_metrics instanceof WP_Error ) {
if ( function_exists( 'wp_trigger_error' ) ) {
wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) );
@@ -248,15 +248,15 @@ function image_loading_optimization_store_page_metric( array $validated_page_met
$page_metrics = array();
}
} else {
- $post_data['post_name'] = image_loading_optimization_get_page_metrics_slug( $url );
+ $post_data['post_name'] = ilo_get_page_metrics_slug( $url );
$page_metrics = array();
}
// Add the provided page metric to the page metrics.
// TODO: Need to implement viewport breakpoint segmenting.
// $segmented_page_metrics =
- // $mobile_max_width = image_loading_optimization_get_max_mobile_viewport_width();
- $viewport_sample_size = image_loading_optimization_get_page_metrics_viewport_sample_size();
+ // $mobile_max_width = ilo_get_max_mobile_viewport_width();
+ $viewport_sample_size = ilo_get_page_metrics_viewport_sample_size();
// $viewport_page_metrics = array();
$page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric.
array_unshift( $page_metrics, $validated_page_metric );
@@ -269,7 +269,7 @@ function image_loading_optimization_store_page_metric( array $validated_page_met
kses_remove_filters();
}
- $post_data['post_type'] = IMAGE_LOADING_OPTIMIZATION_PAGE_METRICS_POST_TYPE;
+ $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE;
$post_data['post_status'] = 'publish';
if ( isset( $post_data['ID'] ) ) {
$result = wp_update_post( wp_slash( $post_data ), true );
From e4d1578188f2b2b11f0ea1dc8d2d9868eaf78eae Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 6 Nov 2023 21:04:57 -0800
Subject: [PATCH 039/371] Segment page metrics by breakpoint and constrain
sample size
---
.../image-loading-optimization/storage.php | 78 ++++++++++++++-----
1 file changed, 59 insertions(+), 19 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index 8e92fc2e01..fd49729f28 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -30,19 +30,35 @@ function ilo_get_page_metric_storage_lock_ttl() {
}
/**
- * Gets the maximum width for a viewport to be considered as a mobile device.
+ * Gets the breakpoint max widths to group page metrics for various viewports.
*
- * @todo This could instead return an array of thresholds, like [ 320, 480, 576 ] which would add additional buckets for small smartphones and phablets in addition to normal smartphones and desktops.
- * @return int Viewport width.
+ * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then
+ * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three
+ * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings:
+ *
+ * 1. 0-320 (small smartphone)
+ * 2. 321-480 (normal smartphone)
+ * 3. 481-576 (phablets)
+ * 4. >576 (desktop)
+ *
+ * @return int[] Breakpoint max widths, sorted in ascending order.
*/
-function ilo_get_max_mobile_viewport_width() {
+function ilo_get_breakpoint_max_widths() {
/**
- * Filters the maximum width for a viewport to be considered as a mobile device.
+ * Filters the breakpoint max widths to group page metrics for various viewports.
*
- * @param int $mobile_max_width Mobile max width.
+ * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
*/
- return (int) apply_filters( 'ilo_max_mobile_viewport_with', 480 );
+ $breakpoint_max_widths = array_map(
+ static function ( $breakpoint_max_width ) {
+ return (int) $breakpoint_max_width;
+ },
+ (array) apply_filters( 'ilo_viewport_breakpoint_max_widths', array( 480 ) )
+ );
+
+ sort( $breakpoint_max_widths );
+ return $breakpoint_max_widths;
}
/**
@@ -116,7 +132,7 @@ function ilo_register_page_metrics_post_type() {
*
* @return int
*/
-function ilo_get_page_metrics_viewport_sample_size() {
+function ilo_get_page_metrics_breakpoint_sample_size() {
/**
* Filters desired sample size for a viewport's page metrics.
*
@@ -197,13 +213,31 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
}
/**
+ * Groups page metrics by breakpoint.
*
- * @todo This needs to take a set of page metrics and segment the individual metrics into breakpoints.
- *
- * @return void
+ * @param array $page_metrics Page metrics.
+ * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
+ * @return array Grouped page metrics.
*/
-function ilo_segment_stored_page_metrics( array $page_metrics, array $breakpoints ) {
-
+function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) {
+ $max_index = count( $breakpoints );
+ $groups = array_fill( 0, $max_index + 1, array() );
+ $largest_breakpoint = $breakpoints[ $max_index - 1 ];
+ foreach ( $page_metrics as $page_metric ) {
+ if ( ! isset( $page_metric['viewport']['width'] ) ) {
+ continue;
+ }
+ $viewport_width = $page_metric['viewport']['width'];
+ if ( $viewport_width > $largest_breakpoint ) {
+ $groups[ $max_index ][] = $page_metric;
+ }
+ foreach ( $breakpoints as $group => $breakpoint ) {
+ if ( $viewport_width <= $breakpoint ) {
+ $groups[ $group ][] = $page_metric;
+ }
+ }
+ }
+ return $groups;
}
/**
@@ -253,14 +287,20 @@ function ilo_store_page_metric( array $validated_page_metric ) {
}
// Add the provided page metric to the page metrics.
- // TODO: Need to implement viewport breakpoint segmenting.
- // $segmented_page_metrics =
- // $mobile_max_width = ilo_get_max_mobile_viewport_width();
- $viewport_sample_size = ilo_get_page_metrics_viewport_sample_size();
- // $viewport_page_metrics = array();
- $page_metrics = array_slice( $page_metrics, 0, $viewport_sample_size - 1 ); // Make room for the additional page metric.
array_unshift( $page_metrics, $validated_page_metric );
+ $breakpoints = ilo_get_breakpoint_max_widths();
+ $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
+ $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints );
+
+ foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) {
+ if ( count( $breakpoint_page_metrics ) > $sample_size ) {
+ $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size );
+ }
+ }
+
+ $page_metrics = array_merge( ...$grouped_page_metrics );
+ // TODO: Also need to capture the current theme and template which can be used to invalidate the cached page metrics.
$post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
From 2b1a15b519d5e62883c5161a7d8c72b1fb9828e6 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 7 Nov 2023 11:26:37 -0800
Subject: [PATCH 040/371] Improve function ordering
---
.../image-loading-optimization/detect.js | 5 +-
.../image-loading-optimization/rest-api.php | 3 -
.../image-loading-optimization/storage.php | 62 +++++++++----------
3 files changed, 33 insertions(+), 37 deletions(-)
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detect.js
index 1bc18b8db2..5971637f13 100644
--- a/modules/images/image-loading-optimization/detect.js
+++ b/modules/images/image-loading-optimization/detect.js
@@ -289,6 +289,8 @@ export default async function detect(
pageMetrics.elements.push( elementMetrics );
}
+ log( pageMetrics );
+
// TODO: Wait until idle.
const response = await fetch( restApiEndpoint, {
method: 'POST',
@@ -300,9 +302,6 @@ export default async function detect(
} );
log( 'response:', await response.json() );
- // TODO: Send data to server.
- log( pageMetrics );
-
// Clean up.
breadcrumbedElementsMap.clear();
}
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/rest-api.php
index 672f5cf9e8..74967a0331 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/rest-api.php
@@ -136,9 +136,6 @@ function ilo_register_endpoint() {
* @return WP_REST_Response|WP_Error Response.
*/
function ilo_handle_rest_request( WP_REST_Request $request ) {
-
- // TODO: We need storage.
-
ilo_set_page_metric_storage_lock();
$result = ilo_store_page_metric( $request->get_json_params() );
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index fd49729f28..fd7ea9fd88 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -12,23 +12,6 @@
define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
-/**
- * Gets the TTL for the page metric storage lock.
- *
- * @return int TTL.
- */
-function ilo_get_page_metric_storage_lock_ttl() {
-
- /**
- * Filters how long a given IP is locked from submitting another metric-storage REST API request.
- *
- * Filtering the TTL to zero will disable any metric storage locking. This is useful during development.
- *
- * @param int $ttl TTL.
- */
- return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
-}
-
/**
* Gets the breakpoint max widths to group page metrics for various viewports.
*
@@ -61,6 +44,37 @@ static function ( $breakpoint_max_width ) {
return $breakpoint_max_widths;
}
+/**
+ * Gets desired sample size for a viewport's page metrics.
+ *
+ * @return int Sample size.
+ */
+function ilo_get_page_metrics_breakpoint_sample_size() {
+ /**
+ * Filters desired sample size for a viewport's page metrics.
+ *
+ * @param int $sample_size Sample size.
+ */
+ return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 );
+}
+
+/**
+ * Gets the TTL for the page metric storage lock.
+ *
+ * @return int TTL.
+ */
+function ilo_get_page_metric_storage_lock_ttl() {
+
+ /**
+ * Filters how long a given IP is locked from submitting another metric-storage REST API request.
+ *
+ * Filtering the TTL to zero will disable any metric storage locking. This is useful during development.
+ *
+ * @param int $ttl TTL.
+ */
+ return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
+}
+
/**
* Gets transient key for locking page metric storage (for the current IP).
*
@@ -127,20 +141,6 @@ function ilo_register_page_metrics_post_type() {
}
add_action( 'init', 'ilo_register_page_metrics_post_type' );
-/**
- * Gets desired sample size for a viewport's page metrics.
- *
- * @return int
- */
-function ilo_get_page_metrics_breakpoint_sample_size() {
- /**
- * Filters desired sample size for a viewport's page metrics.
- *
- * @param int $sample_size Sample size.
- */
- return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 );
-}
-
/**
* Gets slug for page metrics post.
*
From f516af85f463f36cf009a76ab8075475599a268a Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 7 Nov 2023 20:03:46 -0800
Subject: [PATCH 041/371] Add ilo_get_page_metric_ttl
---
.../image-loading-optimization/hooks.php | 1 +
.../image-loading-optimization/storage.php | 25 ++++++++++++++++---
2 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 718cfe8853..59b98e61a7 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -55,6 +55,7 @@ static function ( $output ) {
*/
function ilo_print_detection_script() {
+ // TODO: Also abort if we don't need any new page metrics due to the sample size being full.
if ( ilo_is_page_metric_storage_locked() ) {
return;
}
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index fd7ea9fd88..606200553a 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -37,7 +37,7 @@ function ilo_get_breakpoint_max_widths() {
static function ( $breakpoint_max_width ) {
return (int) $breakpoint_max_width;
},
- (array) apply_filters( 'ilo_viewport_breakpoint_max_widths', array( 480 ) )
+ (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) )
);
sort( $breakpoint_max_widths );
@@ -45,7 +45,7 @@ static function ( $breakpoint_max_width ) {
}
/**
- * Gets desired sample size for a viewport's page metrics.
+ * Gets desired sample size for a breakpoint's page metrics.
*
* @return int Sample size.
*/
@@ -55,7 +55,25 @@ function ilo_get_page_metrics_breakpoint_sample_size() {
*
* @param int $sample_size Sample size.
*/
- return (int) apply_filters( 'ilo_page_metrics_viewport_sample_size', 10 );
+ return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 );
+}
+
+/**
+ * Gets the expiration age for a given page metric.
+ *
+ * When a page metric expires it is eligible to be replaced by a newer one.
+ *
+ * TODO: However, we keep viewport-specific page metrics regardless of TTL.
+ *
+ * @return int Expiration age in seconds.
+ */
+function ilo_get_page_metric_ttl() {
+ /**
+ * Filters the expiration age for a given page metric.
+ *
+ * @param int $ttl TTL.
+ */
+ return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS );
}
/**
@@ -300,7 +318,6 @@ function ilo_store_page_metric( array $validated_page_metric ) {
$page_metrics = array_merge( ...$grouped_page_metrics );
- // TODO: Also need to capture the current theme and template which can be used to invalidate the cached page metrics.
$post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
From 2f804613a2dbdfd6a23746bf4d770b5d675de24e Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 7 Nov 2023 20:08:12 -0800
Subject: [PATCH 042/371] Move lock functions into separate file
---
.../image-loading-optimization/storage.php | 60 +---------------
.../storage/lock.php | 69 +++++++++++++++++++
2 files changed, 71 insertions(+), 58 deletions(-)
create mode 100644 modules/images/image-loading-optimization/storage/lock.php
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index 606200553a..b5d1d37be8 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -10,6 +10,8 @@
exit; // Exit if accessed directly.
}
+require_once __DIR__ . '/storage/lock.php';
+
define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
/**
@@ -76,64 +78,6 @@ function ilo_get_page_metric_ttl() {
return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS );
}
-/**
- * Gets the TTL for the page metric storage lock.
- *
- * @return int TTL.
- */
-function ilo_get_page_metric_storage_lock_ttl() {
-
- /**
- * Filters how long a given IP is locked from submitting another metric-storage REST API request.
- *
- * Filtering the TTL to zero will disable any metric storage locking. This is useful during development.
- *
- * @param int $ttl TTL.
- */
- return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
-}
-
-/**
- * Gets transient key for locking page metric storage (for the current IP).
- *
- * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
- * @return string Transient key.
- */
-function ilo_get_page_metric_storage_lock_transient_key() {
- $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
- return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
-}
-
-/**
- * Sets page metric storage lock (for the current IP).
- */
-function ilo_set_page_metric_storage_lock() {
- $ttl = ilo_get_page_metric_storage_lock_ttl();
- $key = ilo_get_page_metric_storage_lock_transient_key();
- if ( 0 === $ttl ) {
- delete_transient( $key );
- } else {
- set_transient( $key, time(), $ttl );
- }
-}
-
-/**
- * Checks whether page metric storage is locked (for the current IP).
- *
- * @return bool Whether locked.
- */
-function ilo_is_page_metric_storage_locked() {
- $ttl = ilo_get_page_metric_storage_lock_ttl();
- if ( 0 === $ttl ) {
- return false;
- }
- $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() );
- if ( 0 === $locked_time ) {
- return false;
- }
- return time() - $locked_time < $ttl;
-}
-
/**
* Register post type for page metrics storage.
*
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
new file mode 100644
index 0000000000..efbb89424c
--- /dev/null
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -0,0 +1,69 @@
+
Date: Tue, 7 Nov 2023 20:22:11 -0800
Subject: [PATCH 043/371] Reorganize functions into files
---
.../image-loading-optimization/load.php | 1 -
.../image-loading-optimization/storage.php | 276 +-----------------
.../storage/data.php | 125 ++++++++
.../storage/lock.php | 2 +-
.../storage/post-type.php | 180 ++++++++++++
.../{ => storage}/rest-api.php | 3 +-
6 files changed, 311 insertions(+), 276 deletions(-)
create mode 100644 modules/images/image-loading-optimization/storage/data.php
create mode 100644 modules/images/image-loading-optimization/storage/post-type.php
rename modules/images/image-loading-optimization/{ => storage}/rest-api.php (97%)
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 2aaace8afa..68f6287195 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -23,4 +23,3 @@
require_once __DIR__ . '/helper.php';
require_once __DIR__ . '/hooks.php';
require_once __DIR__ . '/storage.php';
-require_once __DIR__ . '/rest-api.php';
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index b5d1d37be8..5ac9fb9220 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -11,276 +11,6 @@
}
require_once __DIR__ . '/storage/lock.php';
-
-define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
-
-/**
- * Gets the breakpoint max widths to group page metrics for various viewports.
- *
- * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then
- * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three
- * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings:
- *
- * 1. 0-320 (small smartphone)
- * 2. 321-480 (normal smartphone)
- * 3. 481-576 (phablets)
- * 4. >576 (desktop)
- *
- * @return int[] Breakpoint max widths, sorted in ascending order.
- */
-function ilo_get_breakpoint_max_widths() {
-
- /**
- * Filters the breakpoint max widths to group page metrics for various viewports.
- *
- * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
- */
- $breakpoint_max_widths = array_map(
- static function ( $breakpoint_max_width ) {
- return (int) $breakpoint_max_width;
- },
- (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) )
- );
-
- sort( $breakpoint_max_widths );
- return $breakpoint_max_widths;
-}
-
-/**
- * Gets desired sample size for a breakpoint's page metrics.
- *
- * @return int Sample size.
- */
-function ilo_get_page_metrics_breakpoint_sample_size() {
- /**
- * Filters desired sample size for a viewport's page metrics.
- *
- * @param int $sample_size Sample size.
- */
- return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 );
-}
-
-/**
- * Gets the expiration age for a given page metric.
- *
- * When a page metric expires it is eligible to be replaced by a newer one.
- *
- * TODO: However, we keep viewport-specific page metrics regardless of TTL.
- *
- * @return int Expiration age in seconds.
- */
-function ilo_get_page_metric_ttl() {
- /**
- * Filters the expiration age for a given page metric.
- *
- * @param int $ttl TTL.
- */
- return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS );
-}
-
-/**
- * Register post type for page metrics storage.
- *
- * This the configuration for this post type is similar to the oembed_cache in core.
- */
-function ilo_register_page_metrics_post_type() {
- register_post_type(
- ILO_PAGE_METRICS_POST_TYPE,
- array(
- 'labels' => array(
- 'name' => __( 'Page Metrics', 'performance-lab' ),
- 'singular_name' => __( 'Page Metrics', 'performance-lab' ),
- ),
- 'public' => false,
- 'hierarchical' => false,
- 'rewrite' => false,
- 'query_var' => false,
- 'delete_with_user' => false,
- 'can_export' => false,
- 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name.
- )
- );
-}
-add_action( 'init', 'ilo_register_page_metrics_post_type' );
-
-/**
- * Gets slug for page metrics post.
- *
- * @param string $url URL.
- * @return string Slug for URL.
- */
-function ilo_get_page_metrics_slug( $url ) {
- return md5( $url );
-}
-
-/**
- * Get page metrics post.
- *
- * @param string $url URL.
- * @return WP_Post|null Post object if exists.
- */
-function ilo_get_page_metrics_post( $url ) {
- $post_query = new WP_Query(
- array(
- 'post_type' => ILO_PAGE_METRICS_POST_TYPE,
- 'post_status' => 'publish',
- 'name' => ilo_get_page_metrics_slug( $url ),
- 'posts_per_page' => 1,
- 'no_found_rows' => true,
- 'cache_results' => true,
- 'update_post_meta_cache' => false,
- 'update_post_term_cache' => false,
- 'lazy_load_term_meta' => false,
- )
- );
-
- $post = array_shift( $post_query->posts );
- if ( $post instanceof WP_Post ) {
- return $post;
- } else {
- return null;
- }
-}
-
-/**
- * Parses post content in page metrics post.
- *
- * @param WP_Post $post Page metrics post.
- * @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
- */
-function ilo_parse_stored_page_metrics( WP_Post $post ) {
- $page_metrics = json_decode( $post->post_content, true );
- if ( json_last_error() ) {
- return new WP_Error(
- 'page_metrics_json_parse_error',
- sprintf(
- /* translators: 1: Post type slug, 2: JSON error message */
- __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
- ILO_PAGE_METRICS_POST_TYPE,
- json_last_error_msg()
- )
- );
- }
- if ( ! is_array( $page_metrics ) ) {
- return new WP_Error(
- 'page_metrics_invalid_data_format',
- sprintf(
- /* translators: %s is post type slug */
- __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
- ILO_PAGE_METRICS_POST_TYPE
- )
- );
- }
- return $page_metrics;
-}
-
-/**
- * Groups page metrics by breakpoint.
- *
- * @param array $page_metrics Page metrics.
- * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
- * @return array Grouped page metrics.
- */
-function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) {
- $max_index = count( $breakpoints );
- $groups = array_fill( 0, $max_index + 1, array() );
- $largest_breakpoint = $breakpoints[ $max_index - 1 ];
- foreach ( $page_metrics as $page_metric ) {
- if ( ! isset( $page_metric['viewport']['width'] ) ) {
- continue;
- }
- $viewport_width = $page_metric['viewport']['width'];
- if ( $viewport_width > $largest_breakpoint ) {
- $groups[ $max_index ][] = $page_metric;
- }
- foreach ( $breakpoints as $group => $breakpoint ) {
- if ( $viewport_width <= $breakpoint ) {
- $groups[ $group ][] = $page_metric;
- }
- }
- }
- return $groups;
-}
-
-/**
- * Stores page metric by merging it with the other page metrics for a given URL.
- *
- * The $validated_page_metric parameter has the following array shape:
- *
- * {
- * 'url': string,
- * 'viewport': array{
- * 'width': int,
- * 'height': int
- * },
- * 'elements': array
- * }
- *
- * @param array $validated_page_metric Page metric, already validated by REST API.
- *
- * @return int|WP_Error Post ID or WP_Error otherwise.
- */
-function ilo_store_page_metric( array $validated_page_metric ) {
- $url = $validated_page_metric['url'];
- unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name.
- $validated_page_metric['timestamp'] = time();
-
- // TODO: What about storing a version identifier?
- $post_data = array(
- 'post_title' => $url,
- );
-
- $post = ilo_get_page_metrics_post( $url );
-
- if ( $post instanceof WP_Post ) {
- $post_data['ID'] = $post->ID;
- $post_data['post_name'] = $post->post_name;
-
- $page_metrics = ilo_parse_stored_page_metrics( $post );
- if ( $page_metrics instanceof WP_Error ) {
- if ( function_exists( 'wp_trigger_error' ) ) {
- wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) );
- }
- $page_metrics = array();
- }
- } else {
- $post_data['post_name'] = ilo_get_page_metrics_slug( $url );
- $page_metrics = array();
- }
-
- // Add the provided page metric to the page metrics.
- array_unshift( $page_metrics, $validated_page_metric );
- $breakpoints = ilo_get_breakpoint_max_widths();
- $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
- $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints );
-
- foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) {
- if ( count( $breakpoint_page_metrics ) > $sample_size ) {
- $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size );
- }
- }
-
- $page_metrics = array_merge( ...$grouped_page_metrics );
-
- $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
-
- $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
- if ( $has_kses ) {
- // Prevent KSES from corrupting JSON in post_content.
- kses_remove_filters();
- }
-
- $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE;
- $post_data['post_status'] = 'publish';
- if ( isset( $post_data['ID'] ) ) {
- $result = wp_update_post( wp_slash( $post_data ), true );
- } else {
- $result = wp_insert_post( wp_slash( $post_data ), true );
- }
-
- if ( $has_kses ) {
- kses_init_filters();
- }
-
- return $result;
-}
+require_once __DIR__ . '/storage/post-type.php';
+require_once __DIR__ . '/storage/data.php';
+require_once __DIR__ . '/storage/rest-api.php';
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
new file mode 100644
index 0000000000..f042422c4b
--- /dev/null
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -0,0 +1,125 @@
+ $sample_size ) {
+ $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size );
+ }
+ }
+
+ return array_merge( ...$grouped_page_metrics );
+}
+
+/**
+ * Gets the breakpoint max widths to group page metrics for various viewports.
+ *
+ * Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then
+ * this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three
+ * provided breakpoints (320, 480, 576) then this means there will be four viewport groupings:
+ *
+ * 1. 0-320 (small smartphone)
+ * 2. 321-480 (normal smartphone)
+ * 3. 481-576 (phablets)
+ * 4. >576 (desktop)
+ *
+ * @return int[] Breakpoint max widths, sorted in ascending order.
+ */
+function ilo_get_breakpoint_max_widths() {
+
+ /**
+ * Filters the breakpoint max widths to group page metrics for various viewports.
+ *
+ * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
+ */
+ $breakpoint_max_widths = array_map(
+ static function ( $breakpoint_max_width ) {
+ return (int) $breakpoint_max_width;
+ },
+ (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) )
+ );
+
+ sort( $breakpoint_max_widths );
+ return $breakpoint_max_widths;
+}
+
+/**
+ * Gets desired sample size for a breakpoint's page metrics.
+ *
+ * @return int Sample size.
+ */
+function ilo_get_page_metrics_breakpoint_sample_size() {
+ /**
+ * Filters desired sample size for a viewport's page metrics.
+ *
+ * @param int $sample_size Sample size.
+ */
+ return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 );
+}
+
+/**
+ * Groups page metrics by breakpoint.
+ *
+ * @param array $page_metrics Page metrics.
+ * @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
+ * @return array Grouped page metrics.
+ */
+function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) {
+ $max_index = count( $breakpoints );
+ $groups = array_fill( 0, $max_index + 1, array() );
+ $largest_breakpoint = $breakpoints[ $max_index - 1 ];
+ foreach ( $page_metrics as $page_metric ) {
+ if ( ! isset( $page_metric['viewport']['width'] ) ) {
+ continue;
+ }
+ $viewport_width = $page_metric['viewport']['width'];
+ if ( $viewport_width > $largest_breakpoint ) {
+ $groups[ $max_index ][] = $page_metric;
+ }
+ foreach ( $breakpoints as $group => $breakpoint ) {
+ if ( $viewport_width <= $breakpoint ) {
+ $groups[ $group ][] = $page_metric;
+ }
+ }
+ }
+ return $groups;
+}
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index efbb89424c..1ae536397b 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -1,6 +1,6 @@
array(
+ 'name' => __( 'Page Metrics', 'performance-lab' ),
+ 'singular_name' => __( 'Page Metrics', 'performance-lab' ),
+ ),
+ 'public' => false,
+ 'hierarchical' => false,
+ 'rewrite' => false,
+ 'query_var' => false,
+ 'delete_with_user' => false,
+ 'can_export' => false,
+ 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name.
+ )
+ );
+}
+add_action( 'init', 'ilo_register_page_metrics_post_type' );
+
+/**
+ * Gets slug for page metrics post.
+ *
+ * @param string $url URL.
+ * @return string Slug for URL.
+ */
+function ilo_get_page_metrics_slug( $url ) {
+ return md5( $url );
+}
+
+/**
+ * Get page metrics post.
+ *
+ * @param string $url URL.
+ * @return WP_Post|null Post object if exists.
+ */
+function ilo_get_page_metrics_post( $url ) {
+ $post_query = new WP_Query(
+ array(
+ 'post_type' => ILO_PAGE_METRICS_POST_TYPE,
+ 'post_status' => 'publish',
+ 'name' => ilo_get_page_metrics_slug( $url ),
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'cache_results' => true,
+ 'update_post_meta_cache' => false,
+ 'update_post_term_cache' => false,
+ 'lazy_load_term_meta' => false,
+ )
+ );
+
+ $post = array_shift( $post_query->posts );
+ if ( $post instanceof WP_Post ) {
+ return $post;
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Parses post content in page metrics post.
+ *
+ * @param WP_Post $post Page metrics post.
+ * @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
+ */
+function ilo_parse_stored_page_metrics( WP_Post $post ) {
+ $page_metrics = json_decode( $post->post_content, true );
+ if ( json_last_error() ) {
+ return new WP_Error(
+ 'page_metrics_json_parse_error',
+ sprintf(
+ /* translators: 1: Post type slug, 2: JSON error message */
+ __( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
+ ILO_PAGE_METRICS_POST_TYPE,
+ json_last_error_msg()
+ )
+ );
+ }
+ if ( ! is_array( $page_metrics ) ) {
+ return new WP_Error(
+ 'page_metrics_invalid_data_format',
+ sprintf(
+ /* translators: %s is post type slug */
+ __( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
+ ILO_PAGE_METRICS_POST_TYPE
+ )
+ );
+ }
+ return $page_metrics;
+}
+
+/**
+ * Stores page metric by merging it with the other page metrics for a given URL.
+ *
+ * The $validated_page_metric parameter has the following array shape:
+ *
+ * {
+ * 'url': string,
+ * 'viewport': array{
+ * 'width': int,
+ * 'height': int
+ * },
+ * 'elements': array
+ * }
+ *
+ * @param array $validated_page_metric Page metric, already validated by REST API.
+ *
+ * @return int|WP_Error Post ID or WP_Error otherwise.
+ */
+function ilo_store_page_metric( array $validated_page_metric ) {
+ $url = $validated_page_metric['url'];
+ unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name.
+ $validated_page_metric['timestamp'] = time();
+
+ // TODO: What about storing a version identifier?
+ $post_data = array(
+ 'post_title' => $url,
+ );
+
+ $post = ilo_get_page_metrics_post( $url );
+
+ if ( $post instanceof WP_Post ) {
+ $post_data['ID'] = $post->ID;
+ $post_data['post_name'] = $post->post_name;
+
+ $page_metrics = ilo_parse_stored_page_metrics( $post );
+ if ( $page_metrics instanceof WP_Error ) {
+ if ( function_exists( 'wp_trigger_error' ) ) {
+ wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) );
+ }
+ $page_metrics = array();
+ }
+ } else {
+ $post_data['post_name'] = ilo_get_page_metrics_slug( $url );
+ $page_metrics = array();
+ }
+
+ $page_metrics = ilo_unshift_page_metrics( $page_metrics, $validated_page_metric );
+
+ $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
+
+ $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
+ if ( $has_kses ) {
+ // Prevent KSES from corrupting JSON in post_content.
+ kses_remove_filters();
+ }
+
+ $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE;
+ $post_data['post_status'] = 'publish';
+ if ( isset( $post_data['ID'] ) ) {
+ $result = wp_update_post( wp_slash( $post_data ), true );
+ } else {
+ $result = wp_insert_post( wp_slash( $post_data ), true );
+ }
+
+ if ( $has_kses ) {
+ kses_init_filters();
+ }
+
+ return $result;
+}
diff --git a/modules/images/image-loading-optimization/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
similarity index 97%
rename from modules/images/image-loading-optimization/rest-api.php
rename to modules/images/image-loading-optimization/storage/rest-api.php
index 74967a0331..42c7d6af74 100644
--- a/modules/images/image-loading-optimization/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -138,7 +138,8 @@ function ilo_register_endpoint() {
function ilo_handle_rest_request( WP_REST_Request $request ) {
ilo_set_page_metric_storage_lock();
- $result = ilo_store_page_metric( $request->get_json_params() );
+ $page_metric = $request->get_json_params();
+ $result = ilo_store_page_metric( $page_metric );
if ( $result instanceof WP_Error ) {
return $result;
From d2455e9c4dfe843e4ed8d4a8f3c80bd52f1a49f0 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 7 Nov 2023 20:26:05 -0800
Subject: [PATCH 044/371] Use PHP 7 const
---
.../images/image-loading-optimization/storage/post-type.php | 6 +++---
.../images/image-loading-optimization/storage/rest-api.php | 5 +++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index c0f6788d87..de9f10d5be 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -10,7 +10,7 @@
exit; // Exit if accessed directly.
}
-define( 'ILO_PAGE_METRICS_POST_TYPE', 'ilo_page_metrics' );
+const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics';
/**
* Register post type for page metrics storage.
@@ -88,7 +88,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
return new WP_Error(
'page_metrics_json_parse_error',
sprintf(
- /* translators: 1: Post type slug, 2: JSON error message */
+ /* translators: 1: Post type slug, 2: JSON error message */
__( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
ILO_PAGE_METRICS_POST_TYPE,
json_last_error_msg()
@@ -99,7 +99,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
return new WP_Error(
'page_metrics_invalid_data_format',
sprintf(
- /* translators: %s is post type slug */
+ /* translators: %s is post type slug */
__( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
ILO_PAGE_METRICS_POST_TYPE
)
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 42c7d6af74..87c6e77da4 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -10,8 +10,9 @@
exit; // Exit if accessed directly.
}
-define( 'ILO_REST_API_NAMESPACE', 'image-loading-optimization/v1' );
-define( 'ILO_PAGE_METRIC_STORAGE_ROUTE', '/image-loading-optimization/page-metric-storage' );
+const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1';
+
+const ILO_PAGE_METRIC_STORAGE_ROUTE = '/image-loading-optimization/page-metric-storage';
/**
* Register endpoint for storage of page metric.
From 79bafac29a0882a1f1f577aab133ee4846a5f0c1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 7 Nov 2023 20:32:56 -0800
Subject: [PATCH 045/371] Improve static analysis
---
.../images/image-loading-optimization/storage/rest-api.php | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 87c6e77da4..facd678dd8 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -39,7 +39,9 @@ function ilo_register_endpoint() {
ILO_PAGE_METRIC_STORAGE_ROUTE,
array(
'methods' => 'POST',
- 'callback' => 'ilo_handle_rest_request',
+ 'callback' => static function ( WP_REST_Request $request ) {
+ return ilo_handle_rest_request( $request );
+ },
'permission_callback' => static function () {
// Needs to be available to unauthenticated visitors.
if ( ilo_is_page_metric_storage_locked() ) {
From c01649442959849de3796239d4092a864bcfdac8 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 8 Nov 2023 11:32:55 -0800
Subject: [PATCH 046/371] Split out detection logic into separate include
---
.../image-loading-optimization/detection.php | 59 +++++++++++++++++++
.../{ => detection}/detect.js | 0
.../image-loading-optimization/hooks.php | 48 ---------------
.../image-loading-optimization/load.php | 1 +
.../image-loading-optimization/storage.php | 2 +-
5 files changed, 61 insertions(+), 49 deletions(-)
create mode 100644 modules/images/image-loading-optimization/detection.php
rename modules/images/image-loading-optimization/{ => detection}/detect.js (100%)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
new file mode 100644
index 0000000000..1cc0567559
--- /dev/null
+++ b/modules/images/image-loading-optimization/detection.php
@@ -0,0 +1,59 @@
+ 'module' )
+ );
+}
+add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' );
diff --git a/modules/images/image-loading-optimization/detect.js b/modules/images/image-loading-optimization/detection/detect.js
similarity index 100%
rename from modules/images/image-loading-optimization/detect.js
rename to modules/images/image-loading-optimization/detection/detect.js
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 59b98e61a7..1f002dab9d 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -46,51 +46,3 @@ static function ( $output ) {
return $passthrough;
}
add_filter( 'template_include', 'ilo_buffer_output', PHP_INT_MAX );
-
-/**
- * Prints the script for detecting loaded images and the LCP element.
- *
- * @todo This should eventually only print the script if metrics are needed.
- * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
- */
-function ilo_print_detection_script() {
-
- // TODO: Also abort if we don't need any new page metrics due to the sample size being full.
- if ( ilo_is_page_metric_storage_locked() ) {
- return;
- }
-
- $serve_time = ceil( microtime( true ) * 1000 );
-
- /**
- * Filters the time window between serve time and run time in which loading detection is allowed to run.
- *
- * Allow this amount of milliseconds between when the page was first generated (and perhaps cached) and when the
- * detect function on the page is allowed to perform its detection logic and submit the request to store the results.
- * This avoids situations in which there is missing detection metrics in which case a site with page caching which
- * also has a lot of traffic could result in a cache stampede.
- *
- * @since n.e.x.t
- * @todo The value should probably be something like the 99th percentile of Time To Last Byte (TTLB) for WordPress sites in CrUX.
- *
- * @param int $detection_time_window Detection time window in milliseconds.
- */
- $detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
-
- $detect_args = array(
- $serve_time,
- $detection_time_window,
- WP_DEBUG,
- rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ),
- wp_create_nonce( 'wp_rest' ),
- );
- wp_print_inline_script_tag(
- sprintf(
- 'import detect from %s; detect( ...%s )',
- wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detect.js' ) ),
- wp_json_encode( $detect_args )
- ),
- array( 'type' => 'module' )
- );
-}
-add_action( 'wp_print_footer_scripts', 'ilo_print_detection_script' );
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 68f6287195..118b22d91b 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -23,3 +23,4 @@
require_once __DIR__ . '/helper.php';
require_once __DIR__ . '/hooks.php';
require_once __DIR__ . '/storage.php';
+require_once __DIR__ . '/detection.php';
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
index 5ac9fb9220..52bbf3b344 100644
--- a/modules/images/image-loading-optimization/storage.php
+++ b/modules/images/image-loading-optimization/storage.php
@@ -1,6 +1,6 @@
Date: Wed, 8 Nov 2023 11:33:41 -0800
Subject: [PATCH 047/371] Remove unused helper.php
---
modules/images/image-loading-optimization/helper.php | 11 -----------
modules/images/image-loading-optimization/load.php | 1 -
2 files changed, 12 deletions(-)
delete mode 100644 modules/images/image-loading-optimization/helper.php
diff --git a/modules/images/image-loading-optimization/helper.php b/modules/images/image-loading-optimization/helper.php
deleted file mode 100644
index 77e6b12b10..0000000000
--- a/modules/images/image-loading-optimization/helper.php
+++ /dev/null
@@ -1,11 +0,0 @@
-
Date: Wed, 8 Nov 2023 11:44:05 -0800
Subject: [PATCH 048/371] Improve logging
---
.../detection/detect.js | 49 ++++++++++++++-----
.../storage/rest-api.php | 5 +-
2 files changed, 39 insertions(+), 15 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 5971637f13..0fa5d7f534 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -25,6 +25,16 @@ function warn( ...message ) {
console.warn( consoleLogPrefix, ...message );
}
+/**
+ * Log an error.
+ *
+ * @param {...*} message
+ */
+function error( ...message ) {
+ // eslint-disable-next-line no-console
+ console.error( consoleLogPrefix, ...message );
+}
+
/**
* @typedef {Object} Breadcrumb
* @property {number} index - Index of element among sibling elements.
@@ -264,7 +274,7 @@ export default async function detect(
);
if ( ! breadcrumbs ) {
if ( isDebug ) {
- warn( 'Unable to look up breadcrumbs for element' );
+ error( 'Unable to look up breadcrumbs for element' );
}
continue;
}
@@ -289,18 +299,33 @@ export default async function detect(
pageMetrics.elements.push( elementMetrics );
}
- log( pageMetrics );
+ if ( isDebug ) {
+ log( 'Page metrics:', pageMetrics );
+ }
- // TODO: Wait until idle.
- const response = await fetch( restApiEndpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-WP-Nonce': restApiNonce,
- },
- body: JSON.stringify( pageMetrics ),
- } );
- log( 'response:', await response.json() );
+ // TODO: Wait until idle? Yield to main?
+ try {
+ const response = await fetch( restApiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': restApiNonce,
+ },
+ body: JSON.stringify( pageMetrics ),
+ } );
+ if ( isDebug ) {
+ const body = await response.json();
+ if ( response.status === 200 ) {
+ log( 'Response:', body );
+ } else {
+ error( 'Failure:', body );
+ }
+ }
+ } catch ( err ) {
+ if ( isDebug ) {
+ error( err );
+ }
+ }
// Clean up.
breadcrumbedElementsMap.clear();
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index facd678dd8..000c00e688 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -148,12 +148,11 @@ function ilo_handle_rest_request( WP_REST_Request $request ) {
return $result;
}
- $response = new WP_REST_Response(
+ return new WP_REST_Response(
array(
'success' => true,
'post_id' => $result,
+ 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['url'] ) ), // TODO: Remove this debug data.
)
);
- $response->set_status( 201 );
- return $response;
}
From a65d0be988ad313f3c851c5bb66e662a1a63d126 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 8 Nov 2023 21:07:53 -0800
Subject: [PATCH 049/371] Restore version constant
---
modules/images/image-loading-optimization/load.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 283d45b2ad..90aac38d17 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -9,11 +9,11 @@
*/
// Define the constant.
-if ( defined( 'ILO_VERSION' ) ) {
+if ( defined( 'IMAGE_LOADING_OPTIMIZATION_VERSION' ) ) {
return;
}
-define( 'ILO_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
+define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
// Do not load the code if it is already loaded through another means.
if ( function_exists( 'ilo_buffer_output' ) ) {
From a17c9a267fe4a31d071f0e59b957ff2f51d1199d Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 8 Nov 2023 21:08:23 -0800
Subject: [PATCH 050/371] Remove redundant plugin short-circuit
---
modules/images/image-loading-optimization/load.php | 5 -----
1 file changed, 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 90aac38d17..6004979d30 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -15,11 +15,6 @@
define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
-// Do not load the code if it is already loaded through another means.
-if ( function_exists( 'ilo_buffer_output' ) ) {
- return;
-}
-
require_once __DIR__ . '/hooks.php';
require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/detection.php';
From 7f6cc2be72512be94acb2781152fde530e656ef7 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 8 Nov 2023 21:13:43 -0800
Subject: [PATCH 051/371] Remove superflous include file
---
.../images/image-loading-optimization/load.php | 8 +++++++-
.../image-loading-optimization/storage.php | 16 ----------------
2 files changed, 7 insertions(+), 17 deletions(-)
delete mode 100644 modules/images/image-loading-optimization/storage.php
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 6004979d30..1086e0ed9d 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -16,5 +16,11 @@
define( 'IMAGE_LOADING_OPTIMIZATION_VERSION', 'Performance Lab ' . PERFLAB_VERSION );
require_once __DIR__ . '/hooks.php';
-require_once __DIR__ . '/storage.php';
+
+// Storage logic.
+require_once __DIR__ . '/storage/lock.php';
+require_once __DIR__ . '/storage/post-type.php';
+require_once __DIR__ . '/storage/data.php';
+require_once __DIR__ . '/storage/rest-api.php';
+
require_once __DIR__ . '/detection.php';
diff --git a/modules/images/image-loading-optimization/storage.php b/modules/images/image-loading-optimization/storage.php
deleted file mode 100644
index 52bbf3b344..0000000000
--- a/modules/images/image-loading-optimization/storage.php
+++ /dev/null
@@ -1,16 +0,0 @@
-
Date: Wed, 8 Nov 2023 21:16:14 -0800
Subject: [PATCH 052/371] Improve page metrics route
---
modules/images/image-loading-optimization/detection.php | 2 +-
.../images/image-loading-optimization/storage/rest-api.php | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 1cc0567559..76dd045198 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -44,7 +44,7 @@ function ilo_print_detection_script() {
$serve_time,
$detection_time_window,
WP_DEBUG,
- rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRIC_STORAGE_ROUTE ),
+ rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
wp_create_nonce( 'wp_rest' ),
);
wp_print_inline_script_tag(
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 000c00e688..4844392d3a 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -12,7 +12,7 @@
const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1';
-const ILO_PAGE_METRIC_STORAGE_ROUTE = '/image-loading-optimization/page-metric-storage';
+const ILO_PAGE_METRICS_ROUTE = '/page-metrics';
/**
* Register endpoint for storage of page metric.
@@ -36,7 +36,7 @@ function ilo_register_endpoint() {
register_rest_route(
ILO_REST_API_NAMESPACE,
- ILO_PAGE_METRIC_STORAGE_ROUTE,
+ ILO_PAGE_METRICS_ROUTE,
array(
'methods' => 'POST',
'callback' => static function ( WP_REST_Request $request ) {
From fb8837b11999f96d9a07c07661767452ad885982 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 8 Nov 2023 21:29:27 -0800
Subject: [PATCH 053/371] Update composer lockfile
---
composer.lock | 73 +++++++++++++++++++++++++++------------------------
1 file changed, 39 insertions(+), 34 deletions(-)
diff --git a/composer.lock b/composer.lock
index 59e7347f06..6a9ae7127c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "8afb8511538e46c6875a017b72ad8711",
+ "content-hash": "2dcd132f2c017c64da30a4a4b6f78f29",
"packages": [
{
"name": "composer/installers",
@@ -236,30 +236,30 @@
},
{
"name": "doctrine/instantiator",
- "version": "2.0.0",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
- "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
"shasum": ""
},
"require": {
- "php": "^8.1"
+ "php": "^7.1 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^11",
+ "doctrine/coding-standard": "^9 || ^11",
"ext-pdo": "*",
"ext-phar": "*",
- "phpbench/phpbench": "^1.2",
- "phpstan/phpstan": "^1.9.4",
- "phpstan/phpstan-phpunit": "^1.3",
- "phpunit/phpunit": "^9.5.27",
- "vimeo/psalm": "^5.4"
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
},
"type": "library",
"autoload": {
@@ -286,7 +286,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
- "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
},
"funding": [
{
@@ -302,7 +302,7 @@
"type": "tidelift"
}
],
- "time": "2022-12-30T00:23:10+00:00"
+ "time": "2022-12-30T00:15:36+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -532,16 +532,16 @@
},
{
"name": "php-stubs/wordpress-stubs",
- "version": "v6.3.0",
+ "version": "v6.4.0",
"source": {
"type": "git",
"url": "https://github.com/php-stubs/wordpress-stubs.git",
- "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c"
+ "reference": "286d42eeb44c6808633cc59b8dbb9aa75fe41264"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c",
- "reference": "adda7609e71d5f4dc7b87c74f8ec9e3437d2e92c",
+ "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/286d42eeb44c6808633cc59b8dbb9aa75fe41264",
+ "reference": "286d42eeb44c6808633cc59b8dbb9aa75fe41264",
"shasum": ""
},
"require-dev": {
@@ -554,6 +554,7 @@
},
"suggest": {
"paragonie/sodium_compat": "Pure PHP implementation of libsodium",
+ "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan"
},
"type": "library",
@@ -570,9 +571,9 @@
],
"support": {
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
- "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.0"
+ "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.4.0"
},
- "time": "2023-08-10T16:34:11+00:00"
+ "time": "2023-11-08T07:02:08+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@@ -865,16 +866,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.10.38",
+ "version": "1.10.41",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "5302bb402c57f00fb3c2c015bac86e0827e4b691"
+ "reference": "c6174523c2a69231df55bdc65b61655e72876d76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/5302bb402c57f00fb3c2c015bac86e0827e4b691",
- "reference": "5302bb402c57f00fb3c2c015bac86e0827e4b691",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6174523c2a69231df55bdc65b61655e72876d76",
+ "reference": "c6174523c2a69231df55bdc65b61655e72876d76",
"shasum": ""
},
"require": {
@@ -923,7 +924,7 @@
"type": "tidelift"
}
],
- "time": "2023-10-06T14:19:14+00:00"
+ "time": "2023-11-05T12:57:57+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -2614,22 +2615,22 @@
},
{
"name": "szepeviktor/phpstan-wordpress",
- "version": "v1.3.0",
+ "version": "v1.3.2",
"source": {
"type": "git",
"url": "https://github.com/szepeviktor/phpstan-wordpress.git",
- "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9"
+ "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9",
- "reference": "5b5cc77ed51fdaf64efe3f00b5aae4b709d2cfa9",
+ "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/b8516ed6bab7ec50aae981698ce3f67f1be2e45a",
+ "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0",
- "phpstan/phpstan": "^1.10.0",
+ "phpstan/phpstan": "^1.10.30",
"symfony/polyfill-php73": "^1.12.0"
},
"require-dev": {
@@ -2640,6 +2641,9 @@
"phpunit/phpunit": "^8.0 || ^9.0",
"szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8"
},
+ "suggest": {
+ "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods"
+ },
"type": "phpstan-extension",
"extra": {
"phpstan": {
@@ -2667,9 +2671,9 @@
],
"support": {
"issues": "https://github.com/szepeviktor/phpstan-wordpress/issues",
- "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.0"
+ "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.2"
},
- "time": "2023-04-23T06:15:06+00:00"
+ "time": "2023-10-16T17:23:56+00:00"
},
{
"name": "theseer/tokenizer",
@@ -2902,8 +2906,9 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=7|^8"
+ "php": ">=7|^8",
+ "ext-json": "*"
},
"platform-dev": [],
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.2.0"
}
From 80c6827277ebcad35c8ebdfe173c773fc8d23d71 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 9 Nov 2023 10:36:29 -0800
Subject: [PATCH 054/371] Pass object to detect() instead of positional args
---
.../image-loading-optimization/detection.php | 12 ++++++------
.../detection/detect.js | 17 +++++++++--------
2 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 76dd045198..3dcd945e91 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -41,15 +41,15 @@ function ilo_print_detection_script() {
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
$detect_args = array(
- $serve_time,
- $detection_time_window,
- WP_DEBUG,
- rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
- wp_create_nonce( 'wp_rest' ),
+ 'serveTime' => $serve_time,
+ 'detectionTimeWindow' => $detection_time_window,
+ 'isDebug' => WP_DEBUG,
+ 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
+ 'restApiNonce' => wp_create_nonce( 'wp_rest' ),
);
wp_print_inline_script_tag(
sprintf(
- 'import detect from %s; detect( ...%s )',
+ 'import detect from %s; detect( %s )',
wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ),
wp_json_encode( $detect_args )
),
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 0fa5d7f534..7498e3d81d 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -98,19 +98,20 @@ function getBreadcrumbs( leafElement ) {
/**
* Detects the LCP element, loaded images, client viewport and store for future optimizations.
*
- * @param {number} serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
- * @param {number} detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
- * @param {boolean} isDebug Whether to show debug messages.
- * @param {string} restApiEndpoint URL for where to send the detection data.
- * @param {string} restApiNonce Nonce for writing to the REST API.
+ * @param {Object} args Args.
+ * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
+ * @param {boolean} args.isDebug Whether to show debug messages.
+ * @param {string} args.restApiEndpoint URL for where to send the detection data.
+ * @param {string} args.restApiNonce Nonce for writing to the REST API.
*/
-export default async function detect(
+export default async function detect( {
serveTime,
detectionTimeWindow,
isDebug,
restApiEndpoint,
- restApiNonce
-) {
+ restApiNonce,
+} ) {
const runTime = new Date().valueOf();
// Abort running detection logic if it was served in a cached page.
From f6208c20ba799d74b2eced8a161145d68cb5c551 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 9 Nov 2023 10:50:11 -0800
Subject: [PATCH 055/371] Add description to
ilo_get_page_metrics_breakpoint_sample_size and reduce default size to 3
---
.../images/image-loading-optimization/storage/data.php | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index f042422c4b..8bb8b3fb68 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -83,17 +83,21 @@ static function ( $breakpoint_max_width ) {
}
/**
- * Gets desired sample size for a breakpoint's page metrics.
+ * Gets the sample size for a breakpoint's page metrics on a given URL.
+ *
+ * A breakpoint divides page metrics for viewports which are smaller and those which are larger. Given the default
+ * sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum
+ * total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop.
*
* @return int Sample size.
*/
function ilo_get_page_metrics_breakpoint_sample_size() {
/**
- * Filters desired sample size for a viewport's page metrics.
+ * Filters the sample size for a breakpoint's page metrics on a given URL.
*
* @param int $sample_size Sample size.
*/
- return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 10 );
+ return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 );
}
/**
From fcf9acd3cdbfd33a940c75ef9088d5f265a0181a Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 9 Nov 2023 15:23:39 -0800
Subject: [PATCH 056/371] Add initial function to get normalized current URL
---
.../storage/data.php | 72 +++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 8bb8b3fb68..5de95f4049 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -28,6 +28,78 @@ function ilo_get_page_metric_ttl() {
return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS );
}
+/**
+ * Gets the normalized current URL.
+ *
+ * TODO: This will need to be made more robust for non-singular URLs. What about multi-faceted archives with multiple taxonomies and date parameters?
+ *
+ * @return string Normalized current URL.
+ */
+function ilo_get_normalized_current_url() {
+ if ( is_singular() ) {
+ $url = wp_get_canonical_url();
+ if ( $url ) {
+ return $url;
+ }
+ }
+
+ $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
+
+ $scheme = is_ssl() ? 'https' : 'http';
+ $host = strtok( $_SERVER['HTTP_HOST'], ':' ); // Use of strtok() since wp-env erroneously includes port in host.
+ $port = (int) $_SERVER['SERVER_PORT'];
+ $path = '';
+ $query = '';
+ if ( preg_match( '%(^.+?)(?:\?([^#]+))?%', wp_unslash( $_SERVER['REQUEST_URI'] ), $matches ) ) {
+ if ( ! empty( $matches[1] ) ) {
+ $path = $matches[1];
+ }
+ if ( ! empty( $matches[2] ) ) {
+ $query = $matches[2];
+ }
+ }
+ if ( $query ) {
+ $removable_query_args = wp_removable_query_args();
+ $removable_query_args[] = 'fbclid';
+
+ $old_query_args = array();
+ $new_query_args = array();
+ wp_parse_str( $query, $old_query_args );
+ foreach ( $old_query_args as $key => $value ) {
+ if (
+ str_starts_with( 'utm_', $key ) ||
+ in_array( $key, $removable_query_args, true )
+ ) {
+ continue;
+ }
+ $new_query_args[ $key ] = $value;
+ }
+ asort( $new_query_args );
+ $query = build_query( $new_query_args );
+ }
+
+ // Normalize open-ended URLs.
+ if ( is_404() ) {
+ $path = $home_path;
+ $query = 'error=404';
+ } elseif ( is_search() ) {
+ $path = $home_path;
+ $query = 's={}';
+ }
+
+ // Rebuild the URL.
+ $url = $scheme . '://' . $host;
+ if ( 80 !== $port && 443 !== $port ) {
+ $url .= ":{$port}";
+ }
+ $url .= $path;
+ if ( $query ) {
+ $url .= "?{$query}";
+ }
+
+ return $url;
+}
+
/**
* Unshift a new page metric onto an array of page metrics.
*
From 224ea516dd0ff470f51f9905d43865c2ee67b9ea Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 10 Nov 2023 11:22:40 -0800
Subject: [PATCH 057/371] Add function for normalizing query vars and getting
current URL
---
.../storage/data.php | 106 +++++++++---------
1 file changed, 51 insertions(+), 55 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 5de95f4049..c2ac6b38e4 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -29,75 +29,71 @@ function ilo_get_page_metric_ttl() {
}
/**
- * Gets the normalized current URL.
+ * Get the URL for the current request.
*
- * TODO: This will need to be made more robust for non-singular URLs. What about multi-faceted archives with multiple taxonomies and date parameters?
+ * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL.
+ * This is needed in particular due to subdirectory installs.
*
- * @return string Normalized current URL.
+ * @return string Current URL.
*/
-function ilo_get_normalized_current_url() {
- if ( is_singular() ) {
- $url = wp_get_canonical_url();
- if ( $url ) {
- return $url;
- }
+function ilo_get_current_url() {
+ $parsed_url = wp_parse_url( home_url() );
+
+ if ( ! is_array( $parsed_url ) ) {
+ $parsed_url = array();
}
- $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
+ if ( empty( $parsed_url['scheme'] ) ) {
+ $parsed_url['scheme'] = is_ssl() ? 'https' : 'http';
+ }
+ if ( ! isset( $parsed_url['host'] ) ) {
+ $parsed_url['host'] = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : 'localhost';
+ }
- $scheme = is_ssl() ? 'https' : 'http';
- $host = strtok( $_SERVER['HTTP_HOST'], ':' ); // Use of strtok() since wp-env erroneously includes port in host.
- $port = (int) $_SERVER['SERVER_PORT'];
- $path = '';
- $query = '';
- if ( preg_match( '%(^.+?)(?:\?([^#]+))?%', wp_unslash( $_SERVER['REQUEST_URI'] ), $matches ) ) {
- if ( ! empty( $matches[1] ) ) {
- $path = $matches[1];
- }
- if ( ! empty( $matches[2] ) ) {
- $query = $matches[2];
+ $current_url = $parsed_url['scheme'] . '://';
+ if ( isset( $parsed_url['user'] ) ) {
+ $current_url .= $parsed_url['user'];
+ if ( isset( $parsed_url['pass'] ) ) {
+ $current_url .= ':' . $parsed_url['pass'];
}
+ $current_url .= '@';
}
- if ( $query ) {
- $removable_query_args = wp_removable_query_args();
- $removable_query_args[] = 'fbclid';
-
- $old_query_args = array();
- $new_query_args = array();
- wp_parse_str( $query, $old_query_args );
- foreach ( $old_query_args as $key => $value ) {
- if (
- str_starts_with( 'utm_', $key ) ||
- in_array( $key, $removable_query_args, true )
- ) {
- continue;
- }
- $new_query_args[ $key ] = $value;
- }
- asort( $new_query_args );
- $query = build_query( $new_query_args );
+ $current_url .= $parsed_url['host'];
+ if ( isset( $parsed_url['port'] ) ) {
+ $current_url .= ':' . $parsed_url['port'];
}
+ $current_url .= '/';
- // Normalize open-ended URLs.
- if ( is_404() ) {
- $path = $home_path;
- $query = 'error=404';
- } elseif ( is_search() ) {
- $path = $home_path;
- $query = 's={}';
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) {
+ $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' );
}
+ return esc_url_raw( $current_url );
+}
- // Rebuild the URL.
- $url = $scheme . '://' . $host;
- if ( 80 !== $port && 443 !== $port ) {
- $url .= ":{$port}";
- }
- $url .= $path;
- if ( $query ) {
- $url .= "?{$query}";
+/**
+ * Gets the normalized query vars for the current request.
+ *
+ * This is used as a cache key for stored page metrics.
+ *
+ * @return array Normalized query vars.
+ */
+function ilo_get_normalized_query_vars() {
+ global $wp;
+
+ // Note that the order of this array is naturally normalized since it is
+ // assembled by iterating over public_query_vars.
+ $normalized_query_vars = $wp->query_vars;
+
+ // Normalize unbounded query vars.
+ if ( is_404() ) {
+ $normalized_query_vars = array(
+ 'error' => 404,
+ );
+ } elseif ( array_key_exists( 's', $normalized_query_vars ) ) {
+ $normalized_query_vars['s'] = '...';
}
- return $url;
+ return $normalized_query_vars;
}
/**
From dd9b3754dc3d3aed99dada94656e9e1988ae1b07 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 10 Nov 2023 11:23:35 -0800
Subject: [PATCH 058/371] Use current() instead of array_shift()
---
modules/images/image-loading-optimization/storage/post-type.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index de9f10d5be..578dcc2db2 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -68,7 +68,7 @@ function ilo_get_page_metrics_post( $url ) {
)
);
- $post = array_shift( $post_query->posts );
+ $post = current( $post_query->posts );
if ( $post instanceof WP_Post ) {
return $post;
} else {
From f9a14939d935e37a360dae450a4a19fff9f92c43 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 10 Nov 2023 12:39:11 -0800
Subject: [PATCH 059/371] Use query vars instead of URL for computing slug; add
HMAC
---
.../image-loading-optimization/detection.php | 5 +++
.../detection/detect.js | 8 ++++-
.../storage/data.php | 24 ++++++++++++++
.../storage/post-type.php | 32 ++++++-------------
.../storage/rest-api.php | 20 ++++++++++--
5 files changed, 64 insertions(+), 25 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 3dcd945e91..00f5e10c72 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -40,12 +40,17 @@ function ilo_print_detection_script() {
*/
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
+ $query_vars = ilo_get_normalized_query_vars();
+ $slug = ilo_get_page_metrics_slug( $query_vars );
+
$detect_args = array(
'serveTime' => $serve_time,
'detectionTimeWindow' => $detection_time_window,
'isDebug' => WP_DEBUG,
'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
'restApiNonce' => wp_create_nonce( 'wp_rest' ),
+ 'pageMetricsSlug' => $slug,
+ 'pageMetricsHmac' => ilo_get_slug_hmac( $slug ), // TODO: Or would a nonce make more sense with the $slug being the action?
);
wp_print_inline_script_tag(
sprintf(
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 7498e3d81d..dec3879ba3 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -104,6 +104,8 @@ function getBreadcrumbs( leafElement ) {
* @param {boolean} args.isDebug Whether to show debug messages.
* @param {string} args.restApiEndpoint URL for where to send the detection data.
* @param {string} args.restApiNonce Nonce for writing to the REST API.
+ * @param {string} args.pageMetricsSlug Slug for page metrics.
+ * @param {string} args.pageMetricsHmac HMAC for the page metric slug.
*/
export default async function detect( {
serveTime,
@@ -111,6 +113,8 @@ export default async function detect( {
isDebug,
restApiEndpoint,
restApiNonce,
+ pageMetricsSlug,
+ pageMetricsHmac,
} ) {
const runTime = new Date().valueOf();
@@ -259,7 +263,9 @@ export default async function detect( {
/** @type {PageMetrics} */
const pageMetrics = {
- url: win.location.href, // TODO: Consider sending canonical URL instead.
+ url: win.location.href,
+ slug: pageMetricsSlug,
+ hmac: pageMetricsHmac,
viewport: {
width: win.innerWidth,
height: win.innerHeight,
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index c2ac6b38e4..63e5ccc3b7 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -96,6 +96,30 @@ function ilo_get_normalized_query_vars() {
return $normalized_query_vars;
}
+/**
+ * Gets slug for page metrics.
+ *
+ * @see ilo_get_normalized_query_vars()
+ *
+ * @param array $query_vars Normalized query vars.
+ * @return string Slug.
+ */
+function ilo_get_page_metrics_slug( $query_vars ) {
+ return md5( wp_json_encode( $query_vars ) );
+}
+
+/**
+ * Compute HMAC for page metrics slug.
+ *
+ * This is used in the REST API to authenticate the storage of new page metrics from a given URL.
+ *
+ * @param string $slug Page metrics slug.
+ * @return false HMAC.
+ */
+function ilo_get_slug_hmac( $slug ) {
+ return hash_hmac( 'sha1', $slug, wp_salt() );
+}
+
/**
* Unshift a new page metric onto an array of page metrics.
*
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index 578dcc2db2..d3bfefdc32 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -37,28 +37,18 @@ function ilo_register_page_metrics_post_type() {
}
add_action( 'init', 'ilo_register_page_metrics_post_type' );
-/**
- * Gets slug for page metrics post.
- *
- * @param string $url URL.
- * @return string Slug for URL.
- */
-function ilo_get_page_metrics_slug( $url ) {
- return md5( $url );
-}
-
/**
* Get page metrics post.
*
- * @param string $url URL.
+ * @param string $slug Page metrics slug.
* @return WP_Post|null Post object if exists.
*/
-function ilo_get_page_metrics_post( $url ) {
+function ilo_get_page_metrics_post( $slug ) {
$post_query = new WP_Query(
array(
'post_type' => ILO_PAGE_METRICS_POST_TYPE,
'post_status' => 'publish',
- 'name' => ilo_get_page_metrics_slug( $url ),
+ 'name' => $slug,
'posts_per_page' => 1,
'no_found_rows' => true,
'cache_results' => true,
@@ -114,7 +104,6 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
* The $validated_page_metric parameter has the following array shape:
*
* {
- * 'url': string,
* 'viewport': array{
* 'width': int,
* 'height': int
@@ -122,21 +111,20 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
* 'elements': array
* }
*
- * @param array $validated_page_metric Page metric, already validated by REST API.
- *
+ * @param string $url URL for the page metrics. This is used purely as metadata.
+ * @param string $slug Page metrics slug (computed from query vars).
+ * @param array $validated_page_metric Page metric, already validated by REST API.
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function ilo_store_page_metric( array $validated_page_metric ) {
- $url = $validated_page_metric['url'];
- unset( $validated_page_metric['url'] ); // Not stored in post_content but rather in post_title/post_name.
+function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) {
$validated_page_metric['timestamp'] = time();
// TODO: What about storing a version identifier?
$post_data = array(
- 'post_title' => $url,
+ 'post_title' => $url, // TODO: Should we keep this? It can help with debugging.
);
- $post = ilo_get_page_metrics_post( $url );
+ $post = ilo_get_page_metrics_post( $slug );
if ( $post instanceof WP_Post ) {
$post_data['ID'] = $post->ID;
@@ -150,7 +138,7 @@ function ilo_store_page_metric( array $validated_page_metric ) {
$page_metrics = array();
}
} else {
- $post_data['post_name'] = ilo_get_page_metrics_slug( $url );
+ $post_data['post_name'] = $slug;
$page_metrics = array();
}
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 4844392d3a..fb2c2c9868 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -65,6 +65,22 @@ function ilo_register_endpoint() {
return true;
},
),
+ 'slug' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'pattern' => '^[0-9a-f]{32}$',
+ ),
+ 'hmac' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'pattern' => '^[0-9a-f]+$',
+ 'validate_callback' => static function ( $hmac, WP_REST_Request $request ) {
+ if ( ! hash_equals( $hmac, ilo_get_slug_hmac( $request->get_param( 'slug' ) ) ) ) {
+ return new WP_Error( 'invalid_hmac', __( 'HMAC comparison failure.', 'performance-lab' ) );
+ }
+ return true;
+ },
+ ),
'viewport' => array(
'description' => __( 'Viewport dimensions', 'performance-lab' ),
'type' => 'object',
@@ -142,7 +158,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) {
ilo_set_page_metric_storage_lock();
$page_metric = $request->get_json_params();
- $result = ilo_store_page_metric( $page_metric );
+ $result = ilo_store_page_metric( $page_metric['url'], $page_metric['slug'], $request->get_json_params() );
if ( $result instanceof WP_Error ) {
return $result;
@@ -152,7 +168,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) {
array(
'success' => true,
'post_id' => $result,
- 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['url'] ) ), // TODO: Remove this debug data.
+ 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['slug'] ) ), // TODO: Remove this debug data.
)
);
}
From 5dc6828bd39683c931f75e0a60250316e145fdaa Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 10 Nov 2023 13:03:35 -0800
Subject: [PATCH 060/371] Use nonce instead of hmac
---
.../image-loading-optimization/detection.php | 2 +-
.../detection/detect.js | 6 ++---
.../storage/data.php | 27 ++++++++++++++++---
.../storage/rest-api.php | 8 +++---
4 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 00f5e10c72..113577ffb0 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -50,7 +50,7 @@ function ilo_print_detection_script() {
'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
'restApiNonce' => wp_create_nonce( 'wp_rest' ),
'pageMetricsSlug' => $slug,
- 'pageMetricsHmac' => ilo_get_slug_hmac( $slug ), // TODO: Or would a nonce make more sense with the $slug being the action?
+ 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ),
);
wp_print_inline_script_tag(
sprintf(
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index dec3879ba3..6379cf7856 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -105,7 +105,7 @@ function getBreadcrumbs( leafElement ) {
* @param {string} args.restApiEndpoint URL for where to send the detection data.
* @param {string} args.restApiNonce Nonce for writing to the REST API.
* @param {string} args.pageMetricsSlug Slug for page metrics.
- * @param {string} args.pageMetricsHmac HMAC for the page metric slug.
+ * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
*/
export default async function detect( {
serveTime,
@@ -114,7 +114,7 @@ export default async function detect( {
restApiEndpoint,
restApiNonce,
pageMetricsSlug,
- pageMetricsHmac,
+ pageMetricsNonce,
} ) {
const runTime = new Date().valueOf();
@@ -265,7 +265,7 @@ export default async function detect( {
const pageMetrics = {
url: win.location.href,
slug: pageMetricsSlug,
- hmac: pageMetricsHmac,
+ nonce: pageMetricsNonce,
viewport: {
width: win.innerWidth,
height: win.innerHeight,
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 63e5ccc3b7..f3813143b6 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -109,15 +109,34 @@ function ilo_get_page_metrics_slug( $query_vars ) {
}
/**
- * Compute HMAC for page metrics slug.
+ * Compute nonce for storing page metrics for a specific slug.
*
* This is used in the REST API to authenticate the storage of new page metrics from a given URL.
*
+ * @see wp_create_nonce()
+ * @see ilo_verify_page_metrics_storage_nonce()
+ *
* @param string $slug Page metrics slug.
- * @return false HMAC.
+ * @return string Nonce.
+ */
+function ilo_get_page_metrics_storage_nonce( $slug ) {
+ return wp_create_nonce( "store_page_metrics:{$slug}" );
+}
+
+/**
+ * Verify nonce for storing page metrics for a specific slug.
+ *
+ * @see wp_verify_nonce()
+ * @see ilo_get_page_metrics_storage_nonce()
+ *
+ * @param string $nonce Page metrics storage nonce.
+ * @param string $slug Page metrics slug.
+ * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago,
+ * 2 if the nonce is valid and generated between 12-24 hours ago.
+ * False if the nonce is invalid.
*/
-function ilo_get_slug_hmac( $slug ) {
- return hash_hmac( 'sha1', $slug, wp_salt() );
+function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) {
+ return wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" );
}
/**
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index fb2c2c9868..2c2816c9a3 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -70,13 +70,13 @@ function ilo_register_endpoint() {
'required' => true,
'pattern' => '^[0-9a-f]{32}$',
),
- 'hmac' => array(
+ 'nonce' => array(
'type' => 'string',
'required' => true,
'pattern' => '^[0-9a-f]+$',
- 'validate_callback' => static function ( $hmac, WP_REST_Request $request ) {
- if ( ! hash_equals( $hmac, ilo_get_slug_hmac( $request->get_param( 'slug' ) ) ) ) {
- return new WP_Error( 'invalid_hmac', __( 'HMAC comparison failure.', 'performance-lab' ) );
+ 'validate_callback' => static function ( $nonce, WP_REST_Request $request ) {
+ if ( ! ilo_verify_page_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) {
+ return new WP_Error( 'invalid_nonce', __( 'Page metrics nonce verification failure.', 'performance-lab' ) );
}
return true;
},
From e0f2b6826ee48502a6ba3594f93f82b4e3c0c877 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 10 Nov 2023 15:19:04 -0800
Subject: [PATCH 061/371] Prevent collecting page metrics when sample size is
full for all breakpoints
---
.../image-loading-optimization/detection.php | 40 ++++++++++++++++---
.../storage/data.php | 40 ++++++++++++-------
.../storage/post-type.php | 20 +++++++++-
.../storage/rest-api.php | 10 +++--
4 files changed, 87 insertions(+), 23 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 113577ffb0..a8a7c3c234 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -17,8 +17,41 @@
* @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
function ilo_print_detection_script() {
+ $query_vars = ilo_get_normalized_query_vars();
+ $slug = ilo_get_page_metrics_slug( $query_vars );
+ $data = ilo_get_page_metrics_data( $slug );
+ if ( ! is_array( $data ) ) {
+ $data = $data;
+ }
+
+ $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() );
+ $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
+ $freshness_ttl = ilo_get_page_metric_freshness_ttl();
+
+ // TODO: This same logic needs to be in the endpoint so that we can reject requests when not needed.
+ $current_time = time();
+ $needed_minimum_viewport_widths = array();
+ foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $page_metrics ) {
+ $needs_page_metrics = false;
+ if ( count( $page_metrics ) < $sample_size ) {
+ $needs_page_metrics = true;
+ } else {
+ foreach ( $page_metrics as $page_metric ) {
+ if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) {
+ $needs_page_metrics = true;
+ break;
+ }
+ }
+ }
+ $needed_minimum_viewport_widths[ $minimum_viewport_width ] = $needs_page_metrics;
+ }
- // TODO: Also abort if we don't need any new page metrics due to the sample size being full.
+ // Abort if we already have all the sample size we need for all breakpoints.
+ if ( count( array_filter( $needed_minimum_viewport_widths ) ) === 0 ) {
+ return;
+ }
+
+ // Abort if storage is locked.
if ( ilo_is_page_metric_storage_locked() ) {
return;
}
@@ -40,9 +73,6 @@ function ilo_print_detection_script() {
*/
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
- $query_vars = ilo_get_normalized_query_vars();
- $slug = ilo_get_page_metrics_slug( $query_vars );
-
$detect_args = array(
'serveTime' => $serve_time,
'detectionTimeWindow' => $detection_time_window,
@@ -54,7 +84,7 @@ function ilo_print_detection_script() {
);
wp_print_inline_script_tag(
sprintf(
- 'import detect from %s; detect( %s )',
+ 'import detect from %s; detect( %s );',
wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ),
wp_json_encode( $detect_args )
),
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index f3813143b6..1eda44737b 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -15,11 +15,9 @@
*
* When a page metric expires it is eligible to be replaced by a newer one.
*
- * TODO: However, we keep viewport-specific page metrics regardless of TTL.
- *
* @return int Expiration age in seconds.
*/
-function ilo_get_page_metric_ttl() {
+function ilo_get_page_metric_freshness_ttl() {
/**
* Filters the expiration age for a given page metric.
*
@@ -216,25 +214,39 @@ function ilo_get_page_metrics_breakpoint_sample_size() {
*
* @param array $page_metrics Page metrics.
* @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
- * @return array Grouped page metrics.
+ * @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within
+ * the breakpoint. The returned array is always one larger than the provided array of breakpoints, since
+ * the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page
+ * metrics with viewports on either side of the breakpoint boundaries.
*/
function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) {
- $max_index = count( $breakpoints );
- $groups = array_fill( 0, $max_index + 1, array() );
- $largest_breakpoint = $breakpoints[ $max_index - 1 ];
+
+ // Convert breakpoint max widths into viewport minimum widths.
+ $viewport_minimum_widths = array_map(
+ static function ( $breakpoint ) {
+ return $breakpoint + 1;
+ },
+ $breakpoints
+ );
+
+ $grouped = array_fill_keys( array_merge( array( 0 ), $viewport_minimum_widths ), array() );
+
foreach ( $page_metrics as $page_metric ) {
if ( ! isset( $page_metric['viewport']['width'] ) ) {
continue;
}
$viewport_width = $page_metric['viewport']['width'];
- if ( $viewport_width > $largest_breakpoint ) {
- $groups[ $max_index ][] = $page_metric;
- }
- foreach ( $breakpoints as $group => $breakpoint ) {
- if ( $viewport_width <= $breakpoint ) {
- $groups[ $group ][] = $page_metric;
+
+ $current_minimum_viewport = 0;
+ foreach ( $viewport_minimum_widths as $viewport_minimum_width ) {
+ if ( $viewport_width > $viewport_minimum_width ) {
+ $current_minimum_viewport = $viewport_minimum_width;
+ } else {
+ break;
}
}
+
+ $grouped[ $current_minimum_viewport ][] = $page_metric;
}
- return $groups;
+ return $grouped;
}
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index d3bfefdc32..c9a0c173ba 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -31,7 +31,7 @@ function ilo_register_page_metrics_post_type() {
'query_var' => false,
'delete_with_user' => false,
'can_export' => false,
- 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the MD5 hash in the post_name.
+ 'supports' => array( 'title' ), // The original URL is stored in the post_title, and the post_name is a hash of the query vars.
)
);
}
@@ -98,6 +98,24 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
return $page_metrics;
}
+/**
+ * Parses post content in page metrics post.
+ *
+ * @param string $slug Page metrics slug.
+ * @return array Page metrics data, or null if invalid.
+ */
+function ilo_get_page_metrics_data( $slug ) {
+ $post = ilo_get_page_metrics_post( $slug );
+ if ( ! ( $post instanceof WP_Post ) ) {
+ return null;
+ }
+ $data = ilo_parse_stored_page_metrics( $post );
+ if ( ! is_array( $data ) ) {
+ return null;
+ }
+ return $data;
+}
+
/**
* Stores page metric by merging it with the other page metrics for a given URL.
*
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 2c2816c9a3..7d48ced227 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -157,8 +157,12 @@ function ilo_register_endpoint() {
function ilo_handle_rest_request( WP_REST_Request $request ) {
ilo_set_page_metric_storage_lock();
- $page_metric = $request->get_json_params();
- $result = ilo_store_page_metric( $page_metric['url'], $page_metric['slug'], $request->get_json_params() );
+ $page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) );
+ $result = ilo_store_page_metric(
+ $request->get_param( 'url' ),
+ $request->get_param( 'slug' ),
+ $page_metric
+ );
if ( $result instanceof WP_Error ) {
return $result;
@@ -168,7 +172,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) {
array(
'success' => true,
'post_id' => $result,
- 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $page_metric['slug'] ) ), // TODO: Remove this debug data.
+ 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data.
)
);
}
From a6b7760bcbe4c44acadb31f656e06993fecb50bc Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 10:58:27 -0800
Subject: [PATCH 062/371] Fix function prefix in tests and self-assignment
---
admin/server-timing.php | 4 ++--
modules/images/image-loading-optimization/detection.php | 2 +-
server-timing/class-perflab-server-timing.php | 2 +-
tests/admin/server-timing-tests.php | 2 +-
.../images/image-loading-optimization/load-tests.php | 6 +++---
5 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/admin/server-timing.php b/admin/server-timing.php
index 46f28df890..85daaf1464 100644
--- a/admin/server-timing.php
+++ b/admin/server-timing.php
@@ -43,7 +43,7 @@ function perflab_add_server_timing_page() {
* @since 2.6.0
*/
function perflab_load_server_timing_page() {
- if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) {
+ if ( ! has_filter( 'template_include', 'ilo_buffer_output' ) ) {
/*
* This settings section technically includes a field, however it is directly rendered as part of the section
* callback due to requiring custom markup.
@@ -95,7 +95,7 @@ static function () {
);
?>
-
+
assertArrayHasKey( PERFLAB_SERVER_TIMING_SCREEN, $wp_settings_sections );
$expected_sections = array( 'benchmarking' );
- if ( ! has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) ) {
+ if ( ! has_filter( 'template_include', 'ilo_buffer_output' ) ) {
$expected_sections[] = 'output-buffering';
}
$this->assertEqualSets(
diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php
index a1a9333c05..3fb443947d 100644
--- a/tests/modules/images/image-loading-optimization/load-tests.php
+++ b/tests/modules/images/image-loading-optimization/load-tests.php
@@ -16,14 +16,14 @@ class Image_Loading_Optimization_Load_Tests extends ImagesTestCase {
* @test
*/
public function it_is_hooking_output_buffering_at_template_include() {
- $this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'image_loading_optimization_buffer_output' ) );
+ $this->assertEquals( PHP_INT_MAX, has_filter( 'template_include', 'ilo_buffer_output' ) );
}
/**
* Make output is buffered and that it is also filtered.
*
* @test
- * @covers ::image_loading_optimization_buffer_output
+ * @covers ::ilo_buffer_output
*/
public function it_buffers_and_filters_output() {
$original = 'Hello World!';
@@ -42,7 +42,7 @@ function ( $buffer ) use ( $original, $expected ) {
);
$original_ob_level = ob_get_level();
- image_loading_optimization_buffer_output();
+ ilo_buffer_output();
$this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' );
echo $original;
From f7aa73b4ee4ec342d933e5a405376e4725371aee Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 11:03:10 -0800
Subject: [PATCH 063/371] Fix filter for ilo_get_page_metric_freshness_ttl;
reduce TTL from month to day
---
.../image-loading-optimization/storage/data.php | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 1eda44737b..91d20d46a7 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -11,19 +11,19 @@
}
/**
- * Gets the expiration age for a given page metric.
+ * Gets the freshness age (TTL) for a given page metric.
*
- * When a page metric expires it is eligible to be replaced by a newer one.
+ * When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint.
*
- * @return int Expiration age in seconds.
+ * @return int Expiration TTL in seconds.
*/
function ilo_get_page_metric_freshness_ttl() {
/**
- * Filters the expiration age for a given page metric.
+ * Filters the freshness age (TTL) for a given page metric.
*
- * @param int $ttl TTL.
+ * @param int $ttl Expiration TTL in seconds.
*/
- return (int) apply_filters( 'ilo_page_metric_ttl', MONTH_IN_SECONDS );
+ return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS );
}
/**
From 8eb0dbef72f7a7957f9fc5660601901b19760d3e Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 17:16:39 -0800
Subject: [PATCH 064/371] Add client-side and server-side checks for whether
page metrics needed for breakpoints
---
.../image-loading-optimization/detection.php | 45 ++++-----------
.../detection/detect.js | 47 +++++++++++++---
.../storage/data.php | 55 +++++++++++++++++++
.../storage/rest-api.php | 15 ++++-
4 files changed, 116 insertions(+), 46 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 91f336793d..2f96406e0f 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -13,41 +13,15 @@
/**
* Prints the script for detecting loaded images and the LCP element.
*
- * @todo This should eventually only print the script if metrics are needed.
* @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
function ilo_print_detection_script() {
$query_vars = ilo_get_normalized_query_vars();
$slug = ilo_get_page_metrics_slug( $query_vars );
- $data = ilo_get_page_metrics_data( $slug );
- if ( ! is_array( $data ) ) {
- $data = array();
- }
-
- $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() );
- $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
- $freshness_ttl = ilo_get_page_metric_freshness_ttl();
-
- // TODO: This same logic needs to be in the endpoint so that we can reject requests when not needed.
- $current_time = time();
- $needed_minimum_viewport_widths = array();
- foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $page_metrics ) {
- $needs_page_metrics = false;
- if ( count( $page_metrics ) < $sample_size ) {
- $needs_page_metrics = true;
- } else {
- foreach ( $page_metrics as $page_metric ) {
- if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) {
- $needs_page_metrics = true;
- break;
- }
- }
- }
- $needed_minimum_viewport_widths[ $minimum_viewport_width ] = $needs_page_metrics;
- }
// Abort if we already have all the sample size we need for all breakpoints.
- if ( count( array_filter( $needed_minimum_viewport_widths ) ) === 0 ) {
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $slug );
+ if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return;
}
@@ -74,13 +48,14 @@ function ilo_print_detection_script() {
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
$detect_args = array(
- 'serveTime' => $serve_time,
- 'detectionTimeWindow' => $detection_time_window,
- 'isDebug' => WP_DEBUG,
- 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
- 'restApiNonce' => wp_create_nonce( 'wp_rest' ),
- 'pageMetricsSlug' => $slug,
- 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ),
+ 'serveTime' => $serve_time,
+ 'detectionTimeWindow' => $detection_time_window,
+ 'isDebug' => WP_DEBUG,
+ 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
+ 'restApiNonce' => wp_create_nonce( 'wp_rest' ),
+ 'pageMetricsSlug' => $slug,
+ 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ),
+ 'neededMinimumViewportWidths' => $needed_minimum_viewport_widths,
);
wp_print_inline_script_tag(
sprintf(
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 6379cf7856..5101f127df 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -95,17 +95,40 @@ function getBreadcrumbs( leafElement ) {
return breadcrumbs;
}
+/**
+ * Checks whether the page metric(s) for the provided viewport width is needed.
+ *
+ * @param {number} viewportWidth - Current viewport width.
+ * @param {Array[]} neededMinimumViewportWidths - Needed minimum viewport widths, in ascending order.
+ * @return {boolean} Whether page metrics are needed.
+ */
+function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) {
+ let lastWasNeeded = false;
+ for ( const [
+ minimumViewportWidth,
+ isNeeded,
+ ] of neededMinimumViewportWidths ) {
+ if ( viewportWidth >= minimumViewportWidth ) {
+ lastWasNeeded = isNeeded;
+ } else {
+ break;
+ }
+ }
+ return lastWasNeeded;
+}
+
/**
* Detects the LCP element, loaded images, client viewport and store for future optimizations.
*
- * @param {Object} args Args.
- * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
- * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
- * @param {boolean} args.isDebug Whether to show debug messages.
- * @param {string} args.restApiEndpoint URL for where to send the detection data.
- * @param {string} args.restApiNonce Nonce for writing to the REST API.
- * @param {string} args.pageMetricsSlug Slug for page metrics.
- * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
+ * @param {Object} args Args.
+ * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
+ * @param {boolean} args.isDebug Whether to show debug messages.
+ * @param {string} args.restApiEndpoint URL for where to send the detection data.
+ * @param {string} args.restApiNonce Nonce for writing to the REST API.
+ * @param {string} args.pageMetricsSlug Slug for page metrics.
+ * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
+ * @param {Array} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics.
*/
export default async function detect( {
serveTime,
@@ -115,6 +138,7 @@ export default async function detect( {
restApiNonce,
pageMetricsSlug,
pageMetricsNonce,
+ neededMinimumViewportWidths, // TODO: The name is not great here.
} ) {
const runTime = new Date().valueOf();
@@ -139,6 +163,13 @@ export default async function detect( {
return;
}
+ if ( ! isViewportNeeded( win.innerWidth, neededMinimumViewportWidths ) ) {
+ if ( isDebug ) {
+ log( 'No need for page metrics from the current viewport.' );
+ }
+ return;
+ }
+
if ( isDebug ) {
log( 'Proceeding with detection' );
}
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 91d20d46a7..1088de68b1 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -250,3 +250,58 @@ static function ( $breakpoint ) {
}
return $grouped;
}
+
+/**
+ * Get needed minimum viewport widths.
+ *
+ * @param string $slug Page metric slug.
+ * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
+ */
+function ilo_get_needed_minimum_viewport_widths( $slug ) {
+ $data = ilo_get_page_metrics_data( $slug );
+ if ( ! is_array( $data ) ) {
+ $data = array();
+ }
+
+ $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() );
+ $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
+ $freshness_ttl = ilo_get_page_metric_freshness_ttl();
+
+ $current_time = time();
+ $needed_minimum_viewport_widths = array();
+ foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) {
+ $needs_page_metrics = false;
+ if ( count( $viewport_page_metrics ) < $sample_size ) {
+ $needs_page_metrics = true;
+ } else {
+ foreach ( $viewport_page_metrics as $page_metric ) {
+ if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) {
+ $needs_page_metrics = true;
+ break;
+ }
+ }
+ }
+ $needed_minimum_viewport_widths[] = array(
+ $minimum_viewport_width,
+ $needs_page_metrics,
+ );
+ }
+
+ return $needed_minimum_viewport_widths;
+}
+
+
+/**
+ * Checks whether there is a page metric needed for one of the breakpoints.
+ *
+ * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
+ * @return bool Whether a page metric is needed.
+ */
+function ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) {
+ foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) {
+ if ( $is_needed ) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 7d48ced227..bcd90347a0 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -155,13 +155,22 @@ function ilo_register_endpoint() {
* @return WP_REST_Response|WP_Error Response.
*/
function ilo_handle_rest_request( WP_REST_Request $request ) {
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $request->get_param( 'slug' ) );
+ if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
+ return new WP_Error(
+ 'no_page_metric_needed',
+ __( 'No page metric needed for any of the breakpoints.', 'performance-lab' ),
+ array( 'status' => 403 )
+ );
+ }
+
ilo_set_page_metric_storage_lock();
+ $new_page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) );
- $page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) );
- $result = ilo_store_page_metric(
+ $result = ilo_store_page_metric(
$request->get_param( 'url' ),
$request->get_param( 'slug' ),
- $page_metric
+ $new_page_metric
);
if ( $result instanceof WP_Error ) {
From 8c6b55984b7604ac01886bf8c87e8720748f7354 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 17:21:49 -0800
Subject: [PATCH 065/371] Remove unused ilo_get_current_url()
---
.../storage/data.php | 43 -------------------
1 file changed, 43 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 1088de68b1..a544363d34 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -26,48 +26,6 @@ function ilo_get_page_metric_freshness_ttl() {
return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS );
}
-/**
- * Get the URL for the current request.
- *
- * This is essentially the REQUEST_URI prefixed by the scheme and host for the home URL.
- * This is needed in particular due to subdirectory installs.
- *
- * @return string Current URL.
- */
-function ilo_get_current_url() {
- $parsed_url = wp_parse_url( home_url() );
-
- if ( ! is_array( $parsed_url ) ) {
- $parsed_url = array();
- }
-
- if ( empty( $parsed_url['scheme'] ) ) {
- $parsed_url['scheme'] = is_ssl() ? 'https' : 'http';
- }
- if ( ! isset( $parsed_url['host'] ) ) {
- $parsed_url['host'] = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : 'localhost';
- }
-
- $current_url = $parsed_url['scheme'] . '://';
- if ( isset( $parsed_url['user'] ) ) {
- $current_url .= $parsed_url['user'];
- if ( isset( $parsed_url['pass'] ) ) {
- $current_url .= ':' . $parsed_url['pass'];
- }
- $current_url .= '@';
- }
- $current_url .= $parsed_url['host'];
- if ( isset( $parsed_url['port'] ) ) {
- $current_url .= ':' . $parsed_url['port'];
- }
- $current_url .= '/';
-
- if ( isset( $_SERVER['REQUEST_URI'] ) ) {
- $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' );
- }
- return esc_url_raw( $current_url );
-}
-
/**
* Gets the normalized query vars for the current request.
*
@@ -290,7 +248,6 @@ function ilo_get_needed_minimum_viewport_widths( $slug ) {
return $needed_minimum_viewport_widths;
}
-
/**
* Checks whether there is a page metric needed for one of the breakpoints.
*
From 7e4064ab11f2befa4e8987e1149a7052019160bb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 17:40:14 -0800
Subject: [PATCH 066/371] Improve testability of
ilo_get_needed_minimum_viewport_widths()
---
.../image-loading-optimization/detection.php | 8 +++++++-
.../storage/data.php | 19 +++++++------------
.../storage/post-type.php | 13 +++++++++----
.../storage/rest-api.php | 8 +++++++-
4 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 2f96406e0f..8bc5bd373f 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -20,7 +20,13 @@ function ilo_print_detection_script() {
$slug = ilo_get_page_metrics_slug( $query_vars );
// Abort if we already have all the sample size we need for all breakpoints.
- $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $slug );
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
+ ilo_get_page_metrics_data( $slug ),
+ time(),
+ ilo_get_breakpoint_max_widths(),
+ ilo_get_page_metrics_breakpoint_sample_size(),
+ ilo_get_page_metric_freshness_ttl()
+ );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return;
}
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index a544363d34..8a8573b7c6 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -212,20 +212,15 @@ static function ( $breakpoint ) {
/**
* Get needed minimum viewport widths.
*
- * @param string $slug Page metric slug.
+ * @param array $page_metrics Page metrics.
+ * @param int $current_time Current time.
+ * @param int[] $breakpoint_max_widths Breakpoint max widths.
+ * @param int $sample_size Sample size for viewports in a breakpoint.
+ * @param int $freshness_ttl Freshness TTL for a page metric.
* @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
*/
-function ilo_get_needed_minimum_viewport_widths( $slug ) {
- $data = ilo_get_page_metrics_data( $slug );
- if ( ! is_array( $data ) ) {
- $data = array();
- }
-
- $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $data, ilo_get_breakpoint_max_widths() );
- $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
- $freshness_ttl = ilo_get_page_metric_freshness_ttl();
-
- $current_time = time();
+function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $breakpoint_max_widths, $sample_size, $freshness_ttl ) {
+ $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths );
$needed_minimum_viewport_widths = array();
foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) {
$needs_page_metrics = false;
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index c9a0c173ba..b5b5a6db29 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -99,19 +99,24 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
}
/**
- * Parses post content in page metrics post.
+ * Gets page metrics for a slug.
+ *
+ * This is a convenience abstractions for lower-level functions.
+ *
+ * @see ilo_get_page_metrics_post()
+ * @see ilo_parse_stored_page_metrics()
*
* @param string $slug Page metrics slug.
- * @return array Page metrics data, or null if invalid.
+ * @return array Page metrics data, or empty array if invalid.
*/
function ilo_get_page_metrics_data( $slug ) {
$post = ilo_get_page_metrics_post( $slug );
if ( ! ( $post instanceof WP_Post ) ) {
- return null;
+ return array();
}
$data = ilo_parse_stored_page_metrics( $post );
if ( ! is_array( $data ) ) {
- return null;
+ return array();
}
return $data;
}
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index bcd90347a0..2e8a0518dd 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -155,7 +155,13 @@ function ilo_register_endpoint() {
* @return WP_REST_Response|WP_Error Response.
*/
function ilo_handle_rest_request( WP_REST_Request $request ) {
- $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths( $request->get_param( 'slug' ) );
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
+ ilo_get_page_metrics_data( $request->get_param( 'slug' ) ),
+ time(),
+ ilo_get_breakpoint_max_widths(),
+ ilo_get_page_metrics_breakpoint_sample_size(),
+ ilo_get_page_metric_freshness_ttl()
+ );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return new WP_Error(
'no_page_metric_needed',
From a1f1aaa9e9261740380558af5c9901661465e6b5 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 13 Nov 2023 18:16:54 -0800
Subject: [PATCH 067/371] Opt for sessionStorage for storage lock for aborting
---
.../image-loading-optimization/detection.php | 6 +-
.../detection/detect.js | 87 ++++++++++++++++---
2 files changed, 76 insertions(+), 17 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 8bc5bd373f..287ef79425 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -31,11 +31,6 @@ function ilo_print_detection_script() {
return;
}
- // Abort if storage is locked.
- if ( ilo_is_page_metric_storage_locked() ) {
- return;
- }
-
$serve_time = ceil( microtime( true ) * 1000 );
/**
@@ -62,6 +57,7 @@ function ilo_print_detection_script() {
'pageMetricsSlug' => $slug,
'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ),
'neededMinimumViewportWidths' => $needed_minimum_viewport_widths,
+ 'storageLockTTL' => ilo_get_page_metric_storage_lock_ttl(),
);
wp_print_inline_script_tag(
sprintf(
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 5101f127df..11bc7d0c3b 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -5,6 +5,43 @@ const doc = win.document;
const consoleLogPrefix = '[Image Loading Optimization]';
+const storageLockTimeSessionKey = 'iloStorageLockTime';
+
+/**
+ * Checks whether storage is locked.
+ *
+ * @param {number} currentTime - Current time in milliseconds.
+ * @param {number} storageLockTTL - Storage lock TTL in seconds.
+ * @return {boolean} Whether storage is locked.
+ */
+function isStorageLocked( currentTime, storageLockTTL ) {
+ try {
+ const storageLockTime = parseInt(
+ sessionStorage.getItem( storageLockTimeSessionKey )
+ );
+ return (
+ ! isNaN( storageLockTime ) &&
+ currentTime < storageLockTime + storageLockTTL * 1000
+ );
+ } catch ( e ) {
+ return false;
+ }
+}
+
+/**
+ * Set the storage lock.
+ *
+ * @param {number} currentTime - Current time in milliseconds.
+ */
+function setStorageLock( currentTime ) {
+ try {
+ sessionStorage.setItem(
+ storageLockTimeSessionKey,
+ String( currentTime )
+ );
+ } catch ( e ) {}
+}
+
/**
* Log a message.
*
@@ -117,18 +154,28 @@ function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) {
return lastWasNeeded;
}
+/**
+ * Gets the current time in milliseconds.
+ *
+ * @return {number} Current time in milliseconds.
+ */
+function getCurrentTime() {
+ return new Date().valueOf();
+}
+
/**
* Detects the LCP element, loaded images, client viewport and store for future optimizations.
*
- * @param {Object} args Args.
- * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
- * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
- * @param {boolean} args.isDebug Whether to show debug messages.
- * @param {string} args.restApiEndpoint URL for where to send the detection data.
- * @param {string} args.restApiNonce Nonce for writing to the REST API.
- * @param {string} args.pageMetricsSlug Slug for page metrics.
- * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
- * @param {Array} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics.
+ * @param {Object} args Args.
+ * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
+ * @param {boolean} args.isDebug Whether to show debug messages.
+ * @param {string} args.restApiEndpoint URL for where to send the detection data.
+ * @param {string} args.restApiNonce Nonce for writing to the REST API.
+ * @param {string} args.pageMetricsSlug Slug for page metrics.
+ * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
+ * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics.
+ * @param {number} args.storageLockTTL The TTL (in seconds) for the page metric storage lock.
*/
export default async function detect( {
serveTime,
@@ -138,12 +185,23 @@ export default async function detect( {
restApiNonce,
pageMetricsSlug,
pageMetricsNonce,
- neededMinimumViewportWidths, // TODO: The name is not great here.
+ neededMinimumViewportWidths,
+ storageLockTTL,
} ) {
- const runTime = new Date().valueOf();
+ const currentTime = getCurrentTime();
+
+ // As an alternative to this, the ilo_print_detection_script() function can short-circuit if the
+ // ilo_is_page_metric_storage_locked() function returns true. However, the downside with that is page caching could
+ // result in metrics being missed being gathered when a user navigates around a site and primes the page cache.
+ if ( isStorageLocked( currentTime, storageLockTTL ) ) {
+ if ( isDebug ) {
+ warn( 'Aborted detection due to storage being locked.' );
+ }
+ return;
+ }
// Abort running detection logic if it was served in a cached page.
- if ( runTime - serveTime > detectionTimeWindow ) {
+ if ( currentTime - serveTime > detectionTimeWindow ) {
if ( isDebug ) {
warn(
'Aborted detection due to being outside detection time window.'
@@ -351,6 +409,11 @@ export default async function detect( {
},
body: JSON.stringify( pageMetrics ),
} );
+
+ if ( response.status === 200 ) {
+ setStorageLock( getCurrentTime() );
+ }
+
if ( isDebug ) {
const body = await response.json();
if ( response.status === 200 ) {
From acf17f97e6ef66a1a499fa04f47efc2d471a02a4 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 10:33:22 -0800
Subject: [PATCH 068/371] Ensure grouped page metrics are sorted by timestamp
before unshifting
---
.../image-loading-optimization/storage/data.php | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 8a8573b7c6..e5294c7380 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -110,6 +110,18 @@ function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) {
foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) {
if ( count( $breakpoint_page_metrics ) > $sample_size ) {
+
+ // Sort page metrics in descending order by timestamp.
+ usort(
+ $breakpoint_page_metrics,
+ static function ( $a, $b ) {
+ if ( ! isset( $a['timestamp'] ) || ! isset( $b['timestamp'] ) ) {
+ return 0;
+ }
+ return $b['timestamp'] <=> $a['timestamp'];
+ }
+ );
+
$breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size );
}
}
From 6cd8198532b7076770d93b2003364949d0d3030f Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 10:36:18 -0800
Subject: [PATCH 069/371] Use microtime(true) instead of time()
---
.../images/image-loading-optimization/detection.php | 7 +++----
.../image-loading-optimization/detection/detect.js | 4 ++--
.../image-loading-optimization/storage/data.php | 2 +-
.../image-loading-optimization/storage/lock.php | 12 ++++++------
.../image-loading-optimization/storage/post-type.php | 2 +-
.../image-loading-optimization/storage/rest-api.php | 2 +-
6 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 287ef79425..ac251f83b9 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -18,11 +18,12 @@
function ilo_print_detection_script() {
$query_vars = ilo_get_normalized_query_vars();
$slug = ilo_get_page_metrics_slug( $query_vars );
+ $microtime = microtime( true );
// Abort if we already have all the sample size we need for all breakpoints.
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
ilo_get_page_metrics_data( $slug ),
- time(),
+ $microtime,
ilo_get_breakpoint_max_widths(),
ilo_get_page_metrics_breakpoint_sample_size(),
ilo_get_page_metric_freshness_ttl()
@@ -31,8 +32,6 @@ function ilo_print_detection_script() {
return;
}
- $serve_time = ceil( microtime( true ) * 1000 );
-
/**
* Filters the time window between serve time and run time in which loading detection is allowed to run.
*
@@ -49,7 +48,7 @@ function ilo_print_detection_script() {
$detection_time_window = apply_filters( 'perflab_image_loading_detection_time_window', 5000 );
$detect_args = array(
- 'serveTime' => $serve_time,
+ 'serveTime' => $microtime * 1000, // In milliseconds for comparison with `Date.now()` in JavaScript.
'detectionTimeWindow' => $detection_time_window,
'isDebug' => WP_DEBUG,
'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 11bc7d0c3b..803919f28c 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -160,14 +160,14 @@ function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) {
* @return {number} Current time in milliseconds.
*/
function getCurrentTime() {
- return new Date().valueOf();
+ return Date.now();
}
/**
* Detects the LCP element, loaded images, client viewport and store for future optimizations.
*
* @param {Object} args Args.
- * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `ceil( microtime( true ) * 1000 )`.
+ * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `microtime( true ) * 1000`.
* @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed.
* @param {boolean} args.isDebug Whether to show debug messages.
* @param {string} args.restApiEndpoint URL for where to send the detection data.
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index e5294c7380..e1a747310f 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -225,7 +225,7 @@ static function ( $breakpoint ) {
* Get needed minimum viewport widths.
*
* @param array $page_metrics Page metrics.
- * @param int $current_time Current time.
+ * @param float $current_time Current time as returned by microtime(true).
* @param int[] $breakpoint_max_widths Breakpoint max widths.
* @param int $sample_size Sample size for viewports in a breakpoint.
* @param int $freshness_ttl Freshness TTL for a page metric.
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index 1ae536397b..f0084b9d38 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -11,9 +11,9 @@
}
/**
- * Gets the TTL for the page metric storage lock.
+ * Gets the TTL (in seconds) for the page metric storage lock.
*
- * @return int TTL.
+ * @return int TTL in seconds.
*/
function ilo_get_page_metric_storage_lock_ttl() {
@@ -47,7 +47,7 @@ function ilo_set_page_metric_storage_lock() {
if ( 0 === $ttl ) {
delete_transient( $key );
} else {
- set_transient( $key, time(), $ttl );
+ set_transient( $key, microtime( true ), $ttl );
}
}
@@ -61,9 +61,9 @@ function ilo_is_page_metric_storage_locked() {
if ( 0 === $ttl ) {
return false;
}
- $locked_time = (int) get_transient( ilo_get_page_metric_storage_lock_transient_key() );
- if ( 0 === $locked_time ) {
+ $locked_time = get_transient( ilo_get_page_metric_storage_lock_transient_key() );
+ if ( false === $locked_time ) {
return false;
}
- return time() - $locked_time < $ttl;
+ return microtime( true ) - floatval( $locked_time ) < $ttl;
}
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index b5b5a6db29..3654cccd1e 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -140,7 +140,7 @@ function ilo_get_page_metrics_data( $slug ) {
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) {
- $validated_page_metric['timestamp'] = time();
+ $validated_page_metric['timestamp'] = microtime( true );
// TODO: What about storing a version identifier?
$post_data = array(
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 2e8a0518dd..0c84c79f51 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -157,7 +157,7 @@ function ilo_register_endpoint() {
function ilo_handle_rest_request( WP_REST_Request $request ) {
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
ilo_get_page_metrics_data( $request->get_param( 'slug' ) ),
- time(),
+ microtime( true ),
ilo_get_breakpoint_max_widths(),
ilo_get_page_metrics_breakpoint_sample_size(),
ilo_get_page_metric_freshness_ttl()
From 9d6707c54ed3e04cd6378f49187e30b54e14337e Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 10:42:03 -0800
Subject: [PATCH 070/371] Reduce code duplication with helper function
---
.../image-loading-optimization/detection.php | 8 +------
.../storage/data.php | 21 +++++++++++++++++++
.../storage/rest-api.php | 8 +------
3 files changed, 23 insertions(+), 14 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index ac251f83b9..f7e929480d 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -21,13 +21,7 @@ function ilo_print_detection_script() {
$microtime = microtime( true );
// Abort if we already have all the sample size we need for all breakpoints.
- $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
- ilo_get_page_metrics_data( $slug ),
- $microtime,
- ilo_get_breakpoint_max_widths(),
- ilo_get_page_metrics_breakpoint_sample_size(),
- ilo_get_page_metric_freshness_ttl()
- );
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return;
}
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index e1a747310f..4ca3081001 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -255,6 +255,27 @@ function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $
return $needed_minimum_viewport_widths;
}
+
+/**
+ * Get needed minimum viewport widths by slug for the current time.
+ *
+ * This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication.
+ *
+ * @see ilo_get_needed_minimum_viewport_widths()
+ *
+ * @param string $slug Page metrics slug.
+ * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
+ */
+function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) {
+ return ilo_get_needed_minimum_viewport_widths(
+ ilo_get_page_metrics_data( $slug ),
+ microtime( true ),
+ ilo_get_breakpoint_max_widths(),
+ ilo_get_page_metrics_breakpoint_sample_size(),
+ ilo_get_page_metric_freshness_ttl()
+ );
+}
+
/**
* Checks whether there is a page metric needed for one of the breakpoints.
*
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 0c84c79f51..9feea484df 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -155,13 +155,7 @@ function ilo_register_endpoint() {
* @return WP_REST_Response|WP_Error Response.
*/
function ilo_handle_rest_request( WP_REST_Request $request ) {
- $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths(
- ilo_get_page_metrics_data( $request->get_param( 'slug' ) ),
- microtime( true ),
- ilo_get_breakpoint_max_widths(),
- ilo_get_page_metrics_breakpoint_sample_size(),
- ilo_get_page_metric_freshness_ttl()
- );
+ $needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return new WP_Error(
'no_page_metric_needed',
From 99ef2996caf805cf2ca0d253374ee66e901cbf84 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 11:28:41 -0800
Subject: [PATCH 071/371] Prevent optimizing search results and add
ilo_can_optimize_response filter
---
.../image-loading-optimization/detection.php | 6 +++--
.../storage/data.php | 23 ++++++++++++++++---
2 files changed, 24 insertions(+), 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index f7e929480d..a8e014e0f5 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -12,10 +12,12 @@
/**
* Prints the script for detecting loaded images and the LCP element.
- *
- * @todo This script should not be printed if the page was requested with non-removal (non-canonical) query args.
*/
function ilo_print_detection_script() {
+ if ( ! ilo_can_optimize_response() ) {
+ return;
+ }
+
$query_vars = ilo_get_normalized_query_vars();
$slug = ilo_get_page_metrics_slug( $query_vars );
$microtime = microtime( true );
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 4ca3081001..7c4c97b6b3 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -26,6 +26,26 @@ function ilo_get_page_metric_freshness_ttl() {
return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS );
}
+/**
+ * Determines whether the current response can be optimized.
+ *
+ * Only search results are not eligible by default for optimization. This is because there is no predictability in
+ * whether posts in the loop will have featured images assigned or not. If a theme template for search results doesn't
+ * even show featured images, then this isn't an issue.
+ *
+ * @return bool Whether response can be optimized.
+ */
+function ilo_can_optimize_response() {
+ $able = ! is_search();
+
+ /**
+ * Filters whether the current response can be optimized.
+ *
+ * @param bool $able Whether response can be optimized.
+ */
+ return (bool) apply_filters( 'ilo_can_optimize_response', $able );
+}
+
/**
* Gets the normalized query vars for the current request.
*
@@ -45,8 +65,6 @@ function ilo_get_normalized_query_vars() {
$normalized_query_vars = array(
'error' => 404,
);
- } elseif ( array_key_exists( 's', $normalized_query_vars ) ) {
- $normalized_query_vars['s'] = '...';
}
return $normalized_query_vars;
@@ -255,7 +273,6 @@ function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $
return $needed_minimum_viewport_widths;
}
-
/**
* Get needed minimum viewport widths by slug for the current time.
*
From 8cb2dcf73ad67a21733f668c8f8554b0bc97baf0 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 11:34:11 -0800
Subject: [PATCH 072/371] Add TODO for ilo_get_normalized_query_vars
---
modules/images/image-loading-optimization/storage/data.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 7c4c97b6b3..b0c2662e38 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -51,6 +51,8 @@ function ilo_can_optimize_response() {
*
* This is used as a cache key for stored page metrics.
*
+ * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache.
+ *
* @return array Normalized query vars.
*/
function ilo_get_normalized_query_vars() {
From a4ffbaef191b7d2b859c0a8bf08834fd57defedf Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 12:55:45 -0800
Subject: [PATCH 073/371] Reference JSON Schema for defintion of function arg
array shape
---
.../image-loading-optimization/storage/data.php | 2 +-
.../image-loading-optimization/storage/post-type.php | 12 +-----------
2 files changed, 2 insertions(+), 12 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index b0c2662e38..804452bf24 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -119,7 +119,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) {
* Unshift a new page metric onto an array of page metrics.
*
* @param array $page_metrics Page metrics.
- * @param array $validated_page_metric Validated page metric.
+ * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return array Updated page metrics.
*/
function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) {
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index 3654cccd1e..5c030941c3 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -124,19 +124,9 @@ function ilo_get_page_metrics_data( $slug ) {
/**
* Stores page metric by merging it with the other page metrics for a given URL.
*
- * The $validated_page_metric parameter has the following array shape:
- *
- * {
- * 'viewport': array{
- * 'width': int,
- * 'height': int
- * },
- * 'elements': array
- * }
- *
* @param string $url URL for the page metrics. This is used purely as metadata.
* @param string $slug Page metrics slug (computed from query vars).
- * @param array $validated_page_metric Page metric, already validated by REST API.
+ * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) {
From ec6e17757c51b558f5bda72c56361af89e52ce4c Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 13:17:42 -0800
Subject: [PATCH 074/371] Add array type declarations
---
.../images/image-loading-optimization/storage/data.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 804452bf24..73283a4066 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -80,7 +80,7 @@ function ilo_get_normalized_query_vars() {
* @param array $query_vars Normalized query vars.
* @return string Slug.
*/
-function ilo_get_page_metrics_slug( $query_vars ) {
+function ilo_get_page_metrics_slug( array $query_vars ) {
return md5( wp_json_encode( $query_vars ) );
}
@@ -122,7 +122,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) {
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return array Updated page metrics.
*/
-function ilo_unshift_page_metrics( $page_metrics, $validated_page_metric ) {
+function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ) {
array_unshift( $page_metrics, $validated_page_metric );
$breakpoints = ilo_get_breakpoint_max_widths();
$sample_size = ilo_get_page_metrics_breakpoint_sample_size();
@@ -251,7 +251,7 @@ static function ( $breakpoint ) {
* @param int $freshness_ttl Freshness TTL for a page metric.
* @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
*/
-function ilo_get_needed_minimum_viewport_widths( $page_metrics, $current_time, $breakpoint_max_widths, $sample_size, $freshness_ttl ) {
+function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_time, array $breakpoint_max_widths, $sample_size, $freshness_ttl ) {
$metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths );
$needed_minimum_viewport_widths = array();
foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) {
@@ -301,7 +301,7 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) {
* @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
* @return bool Whether a page metric is needed.
*/
-function ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) {
+function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ) {
foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) {
if ( $is_needed ) {
return true;
From 817d6825b6542f6bb2e0cbfeefaf4e93d00dd504 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 14:11:15 -0800
Subject: [PATCH 075/371] Add PHP type declarations
---
.../image-loading-optimization/detection.php | 2 +-
.../image-loading-optimization/hooks.php | 10 +++---
.../storage/data.php | 34 +++++++++----------
.../storage/lock.php | 8 ++---
.../storage/post-type.php | 10 +++---
.../storage/rest-api.php | 4 +--
.../image-loading-optimization/load-tests.php | 2 +-
7 files changed, 35 insertions(+), 35 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index a8e014e0f5..d445b1a230 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -13,7 +13,7 @@
/**
* Prints the script for detecting loaded images and the LCP element.
*/
-function ilo_print_detection_script() {
+function ilo_print_detection_script() /*: void (in PHP 7.1) */ {
if ( ! ilo_can_optimize_response() ) {
return;
}
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 1f002dab9d..60565a6119 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -28,19 +28,19 @@
* @since n.e.x.t
* @link https://core.trac.wordpress.org/ticket/43258
*
- * @param mixed $passthrough Optional. Filter value. Default null.
- * @return mixed Unmodified value of $passthrough.
+ * @param string $passthrough Optional. Filter value. Default null.
+ * @return string Unmodified value of $passthrough.
*/
-function ilo_buffer_output( $passthrough = null ) {
+function ilo_buffer_output( string $passthrough ): string {
ob_start(
- static function ( $output ) {
+ static function ( string $output ): string {
/**
* Filters the template output buffer prior to sending to the client.
*
* @param string $output Output buffer.
* @return string Filtered output buffer.
*/
- return apply_filters( 'perflab_template_output_buffer', $output );
+ return (string) apply_filters( 'perflab_template_output_buffer', $output );
}
);
return $passthrough;
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 73283a4066..e91dc17f2a 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -17,7 +17,7 @@
*
* @return int Expiration TTL in seconds.
*/
-function ilo_get_page_metric_freshness_ttl() {
+function ilo_get_page_metric_freshness_ttl(): int {
/**
* Filters the freshness age (TTL) for a given page metric.
*
@@ -35,7 +35,7 @@ function ilo_get_page_metric_freshness_ttl() {
*
* @return bool Whether response can be optimized.
*/
-function ilo_can_optimize_response() {
+function ilo_can_optimize_response(): bool {
$able = ! is_search();
/**
@@ -55,7 +55,7 @@ function ilo_can_optimize_response() {
*
* @return array Normalized query vars.
*/
-function ilo_get_normalized_query_vars() {
+function ilo_get_normalized_query_vars(): array {
global $wp;
// Note that the order of this array is naturally normalized since it is
@@ -80,7 +80,7 @@ function ilo_get_normalized_query_vars() {
* @param array $query_vars Normalized query vars.
* @return string Slug.
*/
-function ilo_get_page_metrics_slug( array $query_vars ) {
+function ilo_get_page_metrics_slug( array $query_vars ): string {
return md5( wp_json_encode( $query_vars ) );
}
@@ -95,7 +95,7 @@ function ilo_get_page_metrics_slug( array $query_vars ) {
* @param string $slug Page metrics slug.
* @return string Nonce.
*/
-function ilo_get_page_metrics_storage_nonce( $slug ) {
+function ilo_get_page_metrics_storage_nonce( string $slug ): string {
return wp_create_nonce( "store_page_metrics:{$slug}" );
}
@@ -107,12 +107,12 @@ function ilo_get_page_metrics_storage_nonce( $slug ) {
*
* @param string $nonce Page metrics storage nonce.
* @param string $slug Page metrics slug.
- * @return int|false 1 if the nonce is valid and generated between 0-12 hours ago,
- * 2 if the nonce is valid and generated between 12-24 hours ago.
- * False if the nonce is invalid.
+ * @return int 1 if the nonce is valid and generated between 0-12 hours ago,
+ * 2 if the nonce is valid and generated between 12-24 hours ago.
+ * 0 if the nonce is invalid.
*/
-function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) {
- return wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" );
+function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): int {
+ return (int) wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" );
}
/**
@@ -122,7 +122,7 @@ function ilo_verify_page_metrics_storage_nonce( $nonce, $slug ) {
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return array Updated page metrics.
*/
-function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ) {
+function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ): array {
array_unshift( $page_metrics, $validated_page_metric );
$breakpoints = ilo_get_breakpoint_max_widths();
$sample_size = ilo_get_page_metrics_breakpoint_sample_size();
@@ -163,7 +163,7 @@ static function ( $a, $b ) {
*
* @return int[] Breakpoint max widths, sorted in ascending order.
*/
-function ilo_get_breakpoint_max_widths() {
+function ilo_get_breakpoint_max_widths(): array {
/**
* Filters the breakpoint max widths to group page metrics for various viewports.
@@ -190,7 +190,7 @@ static function ( $breakpoint_max_width ) {
*
* @return int Sample size.
*/
-function ilo_get_page_metrics_breakpoint_sample_size() {
+function ilo_get_page_metrics_breakpoint_sample_size(): int {
/**
* Filters the sample size for a breakpoint's page metrics on a given URL.
*
@@ -209,7 +209,7 @@ function ilo_get_page_metrics_breakpoint_sample_size() {
* the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page
* metrics with viewports on either side of the breakpoint boundaries.
*/
-function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ) {
+function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ): array {
// Convert breakpoint max widths into viewport minimum widths.
$viewport_minimum_widths = array_map(
@@ -251,7 +251,7 @@ static function ( $breakpoint ) {
* @param int $freshness_ttl Freshness TTL for a page metric.
* @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
*/
-function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_time, array $breakpoint_max_widths, $sample_size, $freshness_ttl ) {
+function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array {
$metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths );
$needed_minimum_viewport_widths = array();
foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) {
@@ -285,7 +285,7 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, $current_t
* @param string $slug Page metrics slug.
* @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
*/
-function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) {
+function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array {
return ilo_get_needed_minimum_viewport_widths(
ilo_get_page_metrics_data( $slug ),
microtime( true ),
@@ -301,7 +301,7 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug ) {
* @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
* @return bool Whether a page metric is needed.
*/
-function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ) {
+function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool {
foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) {
if ( $is_needed ) {
return true;
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index f0084b9d38..b9794394f5 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -15,7 +15,7 @@
*
* @return int TTL in seconds.
*/
-function ilo_get_page_metric_storage_lock_ttl() {
+function ilo_get_page_metric_storage_lock_ttl(): int {
/**
* Filters how long a given IP is locked from submitting another metric-storage REST API request.
@@ -33,7 +33,7 @@ function ilo_get_page_metric_storage_lock_ttl() {
* @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
* @return string Transient key.
*/
-function ilo_get_page_metric_storage_lock_transient_key() {
+function ilo_get_page_metric_storage_lock_transient_key(): string {
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
}
@@ -41,7 +41,7 @@ function ilo_get_page_metric_storage_lock_transient_key() {
/**
* Sets page metric storage lock (for the current IP).
*/
-function ilo_set_page_metric_storage_lock() {
+function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ {
$ttl = ilo_get_page_metric_storage_lock_ttl();
$key = ilo_get_page_metric_storage_lock_transient_key();
if ( 0 === $ttl ) {
@@ -56,7 +56,7 @@ function ilo_set_page_metric_storage_lock() {
*
* @return bool Whether locked.
*/
-function ilo_is_page_metric_storage_locked() {
+function ilo_is_page_metric_storage_locked(): bool {
$ttl = ilo_get_page_metric_storage_lock_ttl();
if ( 0 === $ttl ) {
return false;
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index 5c030941c3..1e0be232e6 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -17,7 +17,7 @@
*
* This the configuration for this post type is similar to the oembed_cache in core.
*/
-function ilo_register_page_metrics_post_type() {
+function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
register_post_type(
ILO_PAGE_METRICS_POST_TYPE,
array(
@@ -43,7 +43,7 @@ function ilo_register_page_metrics_post_type() {
* @param string $slug Page metrics slug.
* @return WP_Post|null Post object if exists.
*/
-function ilo_get_page_metrics_post( $slug ) {
+function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ {
$post_query = new WP_Query(
array(
'post_type' => ILO_PAGE_METRICS_POST_TYPE,
@@ -72,7 +72,7 @@ function ilo_get_page_metrics_post( $slug ) {
* @param WP_Post $post Page metrics post.
* @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
*/
-function ilo_parse_stored_page_metrics( WP_Post $post ) {
+function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in PHP 8) */ {
$page_metrics = json_decode( $post->post_content, true );
if ( json_last_error() ) {
return new WP_Error(
@@ -109,7 +109,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) {
* @param string $slug Page metrics slug.
* @return array Page metrics data, or empty array if invalid.
*/
-function ilo_get_page_metrics_data( $slug ) {
+function ilo_get_page_metrics_data( string $slug ): array {
$post = ilo_get_page_metrics_post( $slug );
if ( ! ( $post instanceof WP_Post ) ) {
return array();
@@ -129,7 +129,7 @@ function ilo_get_page_metrics_data( $slug ) {
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function ilo_store_page_metric( $url, $slug, array $validated_page_metric ) {
+function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) /*: int|WP_Error (in PHP 8) */ {
$validated_page_metric['timestamp'] = microtime( true );
// TODO: What about storing a version identifier?
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 9feea484df..71c1f5300f 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -17,7 +17,7 @@
/**
* Register endpoint for storage of page metric.
*/
-function ilo_register_endpoint() {
+function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
$dom_rect_schema = array(
'type' => 'object',
@@ -154,7 +154,7 @@ function ilo_register_endpoint() {
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
-function ilo_handle_rest_request( WP_REST_Request $request ) {
+function ilo_handle_rest_request( WP_REST_Request $request ) /*: WP_REST_Response|WP_Error (in PHP 8) */ {
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return new WP_Error(
diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php
index 3fb443947d..1015d12b7d 100644
--- a/tests/modules/images/image-loading-optimization/load-tests.php
+++ b/tests/modules/images/image-loading-optimization/load-tests.php
@@ -42,7 +42,7 @@ function ( $buffer ) use ( $original, $expected ) {
);
$original_ob_level = ob_get_level();
- ilo_buffer_output();
+ ilo_buffer_output( '' );
$this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' );
echo $original;
From caaef872dbd05595a730a91ad9efee2ad47caefd Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 18:10:24 -0800
Subject: [PATCH 076/371] Fix placement of ilo_breakpoint_max_widths filter
---
.../images/image-loading-optimization/storage/data.php | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index e91dc17f2a..96f642c0a9 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -165,15 +165,15 @@ static function ( $a, $b ) {
*/
function ilo_get_breakpoint_max_widths(): array {
- /**
- * Filters the breakpoint max widths to group page metrics for various viewports.
- *
- * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
- */
$breakpoint_max_widths = array_map(
static function ( $breakpoint_max_width ) {
return (int) $breakpoint_max_width;
},
+ /**
+ * Filters the breakpoint max widths to group page metrics for various viewports.
+ *
+ * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
+ */
(array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) )
);
From eadec5592a8b103e3522fa9f29b75e5b8dcea2e9 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 18:11:43 -0800
Subject: [PATCH 077/371] Use 3rd person singular for function phpdoc
Co-authored-by: Felix Arntz
---
modules/images/image-loading-optimization/storage/data.php | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 96f642c0a9..b90953e45d 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -85,7 +85,7 @@ function ilo_get_page_metrics_slug( array $query_vars ): string {
}
/**
- * Compute nonce for storing page metrics for a specific slug.
+ * Computes nonce for storing page metrics for a specific slug.
*
* This is used in the REST API to authenticate the storage of new page metrics from a given URL.
*
@@ -100,7 +100,7 @@ function ilo_get_page_metrics_storage_nonce( string $slug ): string {
}
/**
- * Verify nonce for storing page metrics for a specific slug.
+ * Verifies nonce for storing page metrics for a specific slug.
*
* @see wp_verify_nonce()
* @see ilo_get_page_metrics_storage_nonce()
@@ -116,7 +116,7 @@ function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): i
}
/**
- * Unshift a new page metric onto an array of page metrics.
+ * Unshifts a new page metric onto an array of page metrics.
*
* @param array $page_metrics Page metrics.
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
From 02dbb359fa726ed889475f9cad66bb362f9b36e8 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 18:15:25 -0800
Subject: [PATCH 078/371] Use 3rd person singular in phpdoc for additional
functions
---
modules/images/image-loading-optimization/storage/data.php | 4 ++--
.../images/image-loading-optimization/storage/post-type.php | 4 ++--
.../images/image-loading-optimization/storage/rest-api.php | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index b90953e45d..25d3bca571 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -242,7 +242,7 @@ static function ( $breakpoint ) {
}
/**
- * Get needed minimum viewport widths.
+ * Gets needed minimum viewport widths.
*
* @param array $page_metrics Page metrics.
* @param float $current_time Current time as returned by microtime(true).
@@ -276,7 +276,7 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur
}
/**
- * Get needed minimum viewport widths by slug for the current time.
+ * Gets needed minimum viewport widths by slug for the current time.
*
* This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication.
*
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index 1e0be232e6..c3a840e240 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -13,7 +13,7 @@
const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics';
/**
- * Register post type for page metrics storage.
+ * Registers post type for page metrics storage.
*
* This the configuration for this post type is similar to the oembed_cache in core.
*/
@@ -38,7 +38,7 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
add_action( 'init', 'ilo_register_page_metrics_post_type' );
/**
- * Get page metrics post.
+ * Gets page metrics post.
*
* @param string $slug Page metrics slug.
* @return WP_Post|null Post object if exists.
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 71c1f5300f..9726f04087 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -15,7 +15,7 @@
const ILO_PAGE_METRICS_ROUTE = '/page-metrics';
/**
- * Register endpoint for storage of page metric.
+ * Registers endpoint for storage of page metric.
*/
function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
@@ -149,7 +149,7 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
add_action( 'rest_api_init', 'ilo_register_endpoint' );
/**
- * Handle REST API request to store metrics.
+ * Handles REST API request to store metrics.
*
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
From 06d7fad7f75e02a0f3b2e572ba6d289065515fd1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 18:17:39 -0800
Subject: [PATCH 079/371] Account for isStorageLocked with storageLockTTL of 0
Co-authored-by: Felix Arntz
---
modules/images/image-loading-optimization/detection/detect.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 803919f28c..58a0034376 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -15,6 +15,10 @@ const storageLockTimeSessionKey = 'iloStorageLockTime';
* @return {boolean} Whether storage is locked.
*/
function isStorageLocked( currentTime, storageLockTTL ) {
+ if ( ! storageLockTTL ) {
+ return false;
+ }
+
try {
const storageLockTime = parseInt(
sessionStorage.getItem( storageLockTimeSessionKey )
From da0b4208b73694d1e1800cb04178140b9837b094 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 19:55:14 -0800
Subject: [PATCH 080/371] Ensure storage lock TTL is at least zero and add
filter example
---
.../detection/detect.js | 2 +-
.../image-loading-optimization/storage/lock.php | 15 ++++++++++++---
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 58a0034376..96d0587a4b 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -15,7 +15,7 @@ const storageLockTimeSessionKey = 'iloStorageLockTime';
* @return {boolean} Whether storage is locked.
*/
function isStorageLocked( currentTime, storageLockTTL ) {
- if ( ! storageLockTTL ) {
+ if ( storageLockTTL === 0 ) {
return false;
}
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index b9794394f5..eb6b21ca68 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -13,18 +13,24 @@
/**
* Gets the TTL (in seconds) for the page metric storage lock.
*
- * @return int TTL in seconds.
+ * @return int TTL in seconds, greater than or equal to zero.
*/
function ilo_get_page_metric_storage_lock_ttl(): int {
/**
* Filters how long a given IP is locked from submitting another metric-storage REST API request.
*
- * Filtering the TTL to zero will disable any metric storage locking. This is useful during development.
+ * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable
+ * locking when a user is logged-in with code like the following:
+ *
+ * add_filter( 'ilo_metrics_storage_lock_ttl', static function ( $ttl ) {
+ * return is_user_logged_in() ? 0 : $ttl;
+ * } );
*
* @param int $ttl TTL.
*/
- return (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
+ $ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
+ return max( 0, $ttl );
}
/**
@@ -40,6 +46,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string {
/**
* Sets page metric storage lock (for the current IP).
+ *
+ * If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL
+ * seconds. Otherwise, if the current TTL is zero, then any transient is deleted.
*/
function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ {
$ttl = ilo_get_page_metric_storage_lock_ttl();
From 19ce75252cd1ef2a414e421d1c8a71024a5d1a32 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 20:23:08 -0800
Subject: [PATCH 081/371] Add since and access private tags
---
.../image-loading-optimization/detection.php | 3 ++
.../image-loading-optimization/hooks.php | 5 ++-
.../storage/data.php | 45 +++++++++++++++++++
.../storage/lock.php | 11 +++++
.../storage/post-type.php | 15 +++++++
.../storage/rest-api.php | 6 +++
6 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index d445b1a230..db8b3a658a 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -12,6 +12,9 @@
/**
* Prints the script for detecting loaded images and the LCP element.
+ *
+ * @since n.e.x.t
+ * @access private
*/
function ilo_print_detection_script() /*: void (in PHP 7.1) */ {
if ( ! ilo_can_optimize_response() ) {
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 60565a6119..426f20b646 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -17,15 +17,14 @@
*
* This is a hack which would eventually be replaced with something like this in wp-includes/template-loader.php:
*
- * ```
* $template = apply_filters( 'template_include', $template );
* + ob_start( 'wp_template_output_buffer_callback' );
* if ( $template ) {
* include $template;
* } elseif ( current_user_can( 'switch_themes' ) ) {
- * ```
*
* @since n.e.x.t
+ * @access private
* @link https://core.trac.wordpress.org/ticket/43258
*
* @param string $passthrough Optional. Filter value. Default null.
@@ -37,6 +36,8 @@ static function ( string $output ): string {
/**
* Filters the template output buffer prior to sending to the client.
*
+ * @since n.e.x.t
+ *
* @param string $output Output buffer.
* @return string Filtered output buffer.
*/
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 25d3bca571..b562d2784e 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -15,12 +15,17 @@
*
* When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return int Expiration TTL in seconds.
*/
function ilo_get_page_metric_freshness_ttl(): int {
/**
* Filters the freshness age (TTL) for a given page metric.
*
+ * @since n.e.x.t
+ *
* @param int $ttl Expiration TTL in seconds.
*/
return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS );
@@ -33,6 +38,9 @@ function ilo_get_page_metric_freshness_ttl(): int {
* whether posts in the loop will have featured images assigned or not. If a theme template for search results doesn't
* even show featured images, then this isn't an issue.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return bool Whether response can be optimized.
*/
function ilo_can_optimize_response(): bool {
@@ -41,6 +49,8 @@ function ilo_can_optimize_response(): bool {
/**
* Filters whether the current response can be optimized.
*
+ * @since n.e.x.t
+ *
* @param bool $able Whether response can be optimized.
*/
return (bool) apply_filters( 'ilo_can_optimize_response', $able );
@@ -53,6 +63,9 @@ function ilo_can_optimize_response(): bool {
*
* TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return array Normalized query vars.
*/
function ilo_get_normalized_query_vars(): array {
@@ -75,6 +88,9 @@ function ilo_get_normalized_query_vars(): array {
/**
* Gets slug for page metrics.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @see ilo_get_normalized_query_vars()
*
* @param array $query_vars Normalized query vars.
@@ -89,6 +105,9 @@ function ilo_get_page_metrics_slug( array $query_vars ): string {
*
* This is used in the REST API to authenticate the storage of new page metrics from a given URL.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @see wp_create_nonce()
* @see ilo_verify_page_metrics_storage_nonce()
*
@@ -102,6 +121,9 @@ function ilo_get_page_metrics_storage_nonce( string $slug ): string {
/**
* Verifies nonce for storing page metrics for a specific slug.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @see wp_verify_nonce()
* @see ilo_get_page_metrics_storage_nonce()
*
@@ -118,6 +140,9 @@ function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): i
/**
* Unshifts a new page metric onto an array of page metrics.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $page_metrics Page metrics.
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return array Updated page metrics.
@@ -161,6 +186,9 @@ static function ( $a, $b ) {
* 3. 481-576 (phablets)
* 4. >576 (desktop)
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return int[] Breakpoint max widths, sorted in ascending order.
*/
function ilo_get_breakpoint_max_widths(): array {
@@ -188,12 +216,17 @@ static function ( $breakpoint_max_width ) {
* sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum
* total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return int Sample size.
*/
function ilo_get_page_metrics_breakpoint_sample_size(): int {
/**
* Filters the sample size for a breakpoint's page metrics on a given URL.
*
+ * @since n.e.x.t
+ *
* @param int $sample_size Sample size.
*/
return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 );
@@ -202,6 +235,9 @@ function ilo_get_page_metrics_breakpoint_sample_size(): int {
/**
* Groups page metrics by breakpoint.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $page_metrics Page metrics.
* @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
* @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within
@@ -244,6 +280,9 @@ static function ( $breakpoint ) {
/**
* Gets needed minimum viewport widths.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $page_metrics Page metrics.
* @param float $current_time Current time as returned by microtime(true).
* @param int[] $breakpoint_max_widths Breakpoint max widths.
@@ -280,6 +319,9 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur
*
* This is a convenience wrapper on top of ilo_get_needed_minimum_viewport_widths() to reduce code duplication.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @see ilo_get_needed_minimum_viewport_widths()
*
* @param string $slug Page metrics slug.
@@ -298,6 +340,9 @@ function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): ar
/**
* Checks whether there is a page metric needed for one of the breakpoints.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
* @return bool Whether a page metric is needed.
*/
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index eb6b21ca68..5b9307fdad 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -13,6 +13,9 @@
/**
* Gets the TTL (in seconds) for the page metric storage lock.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return int TTL in seconds, greater than or equal to zero.
*/
function ilo_get_page_metric_storage_lock_ttl(): int {
@@ -27,6 +30,8 @@ function ilo_get_page_metric_storage_lock_ttl(): int {
* return is_user_logged_in() ? 0 : $ttl;
* } );
*
+ * @since n.e.x.t
+ *
* @param int $ttl TTL.
*/
$ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
@@ -49,6 +54,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string {
*
* If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL
* seconds. Otherwise, if the current TTL is zero, then any transient is deleted.
+ *
+ * @since n.e.x.t
+ * @access private
*/
function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ {
$ttl = ilo_get_page_metric_storage_lock_ttl();
@@ -63,6 +71,9 @@ function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ {
/**
* Checks whether page metric storage is locked (for the current IP).
*
+ * @since n.e.x.t
+ * @access private
+ *
* @return bool Whether locked.
*/
function ilo_is_page_metric_storage_locked(): bool {
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index c3a840e240..fce053b38d 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -16,6 +16,9 @@
* Registers post type for page metrics storage.
*
* This the configuration for this post type is similar to the oembed_cache in core.
+ *
+ * @since n.e.x.t
+ * @access private
*/
function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
register_post_type(
@@ -40,6 +43,9 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
/**
* Gets page metrics post.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param string $slug Page metrics slug.
* @return WP_Post|null Post object if exists.
*/
@@ -69,6 +75,9 @@ function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */
/**
* Parses post content in page metrics post.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param WP_Post $post Page metrics post.
* @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
*/
@@ -103,6 +112,9 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in P
*
* This is a convenience abstractions for lower-level functions.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @see ilo_get_page_metrics_post()
* @see ilo_parse_stored_page_metrics()
*
@@ -124,6 +136,9 @@ function ilo_get_page_metrics_data( string $slug ): array {
/**
* Stores page metric by merging it with the other page metrics for a given URL.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param string $url URL for the page metrics. This is used purely as metadata.
* @param string $slug Page metrics slug (computed from query vars).
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 9726f04087..8cd9848c22 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -16,6 +16,9 @@
/**
* Registers endpoint for storage of page metric.
+ *
+ * @since n.e.x.t
+ * @access private
*/
function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
@@ -151,6 +154,9 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
/**
* Handles REST API request to store metrics.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
From 19a2c3b274b553074f212b053c66f13fe665a148 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 14 Nov 2023 20:26:13 -0800
Subject: [PATCH 082/371] Use IMAGE_LOADING_OPTIMIZATION_VERSION constant for
script ver arg
---
modules/images/image-loading-optimization/detection.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index db8b3a658a..47e770e9a9 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -60,7 +60,7 @@ function ilo_print_detection_script() /*: void (in PHP 7.1) */ {
wp_print_inline_script_tag(
sprintf(
'import detect from %s; detect( %s );',
- wp_json_encode( add_query_arg( 'ver', PERFLAB_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ),
+ wp_json_encode( add_query_arg( 'ver', IMAGE_LOADING_OPTIMIZATION_VERSION, plugin_dir_url( __FILE__ ) . 'detection/detect.js' ) ),
wp_json_encode( $detect_args )
),
array( 'type' => 'module' )
From f566663483409664c0776989df918a9a08635dff Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 09:52:00 -0800
Subject: [PATCH 083/371] Move error emitting to
ilo_parse_stored_page_metrics()
---
.../storage/data.php | 3 +-
.../storage/post-type.php | 57 +++++--------------
2 files changed, 17 insertions(+), 43 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index b562d2784e..211d1210f0 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -328,8 +328,9 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur
* @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
*/
function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array {
+ $post = ilo_get_page_metrics_post( $slug );
return ilo_get_needed_minimum_viewport_widths(
- ilo_get_page_metrics_data( $slug ),
+ $post instanceof WP_Post ? ilo_parse_stored_page_metrics( $post ) : array(),
microtime( true ),
ilo_get_breakpoint_max_widths(),
ilo_get_page_metrics_breakpoint_sample_size(),
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index fce053b38d..e712678d2b 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -79,13 +79,19 @@ function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */
* @access private
*
* @param WP_Post $post Page metrics post.
- * @return array|WP_Error Page metrics when valid, or WP_Error otherwise.
+ * @return array Page metrics.
*/
-function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in PHP 8) */ {
+function ilo_parse_stored_page_metrics( WP_Post $post ): array {
+ $this_function = __FUNCTION__;
+ $trigger_error = static function ( $error ) use ( $this_function ) {
+ if ( function_exists( 'wp_trigger_error' ) ) {
+ wp_trigger_error( $this_function, esc_html( $error ), E_USER_WARNING );
+ }
+ };
+
$page_metrics = json_decode( $post->post_content, true );
if ( json_last_error() ) {
- return new WP_Error(
- 'page_metrics_json_parse_error',
+ $trigger_error(
sprintf(
/* translators: 1: Post type slug, 2: JSON error message */
__( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
@@ -93,46 +99,20 @@ function ilo_parse_stored_page_metrics( WP_Post $post ) /*: array|WP_Error (in P
json_last_error_msg()
)
);
- }
- if ( ! is_array( $page_metrics ) ) {
- return new WP_Error(
- 'page_metrics_invalid_data_format',
+ $page_metrics = array();
+ } elseif ( ! is_array( $page_metrics ) ) {
+ $trigger_error(
sprintf(
/* translators: %s is post type slug */
__( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
ILO_PAGE_METRICS_POST_TYPE
)
);
+ $page_metrics = array();
}
return $page_metrics;
}
-/**
- * Gets page metrics for a slug.
- *
- * This is a convenience abstractions for lower-level functions.
- *
- * @since n.e.x.t
- * @access private
- *
- * @see ilo_get_page_metrics_post()
- * @see ilo_parse_stored_page_metrics()
- *
- * @param string $slug Page metrics slug.
- * @return array Page metrics data, or empty array if invalid.
- */
-function ilo_get_page_metrics_data( string $slug ): array {
- $post = ilo_get_page_metrics_post( $slug );
- if ( ! ( $post instanceof WP_Post ) ) {
- return array();
- }
- $data = ilo_parse_stored_page_metrics( $post );
- if ( ! is_array( $data ) ) {
- return array();
- }
- return $data;
-}
-
/**
* Stores page metric by merging it with the other page metrics for a given URL.
*
@@ -157,14 +137,7 @@ function ilo_store_page_metric( string $url, string $slug, array $validated_page
if ( $post instanceof WP_Post ) {
$post_data['ID'] = $post->ID;
$post_data['post_name'] = $post->post_name;
-
- $page_metrics = ilo_parse_stored_page_metrics( $post );
- if ( $page_metrics instanceof WP_Error ) {
- if ( function_exists( 'wp_trigger_error' ) ) {
- wp_trigger_error( __FUNCTION__, esc_html( $page_metrics->get_error_message() ) );
- }
- $page_metrics = array();
- }
+ $page_metrics = ilo_parse_stored_page_metrics( $post );
} else {
$post_data['post_name'] = $slug;
$page_metrics = array();
From b228934d938040485671559145b5044c2b656497 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 11:24:18 -0800
Subject: [PATCH 084/371] Elaborate on return value phpdoc for
ilo_get_page_metric_storage_lock_ttl()
Co-authored-by: Felix Arntz
---
modules/images/image-loading-optimization/storage/lock.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index 5b9307fdad..97902d4a30 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -16,7 +16,7 @@
* @since n.e.x.t
* @access private
*
- * @return int TTL in seconds, greater than or equal to zero.
+ * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used.
*/
function ilo_get_page_metric_storage_lock_ttl(): int {
From 95d940603cecfeacbdddc64d04384acce303cc54 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 11:27:11 -0800
Subject: [PATCH 085/371] Remove overkill PHP 7.1+ type declaration comments
---
modules/images/image-loading-optimization/detection.php | 2 +-
modules/images/image-loading-optimization/storage/lock.php | 2 +-
.../images/image-loading-optimization/storage/post-type.php | 6 +++---
.../images/image-loading-optimization/storage/rest-api.php | 4 ++--
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 47e770e9a9..90a1ffc461 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -16,7 +16,7 @@
* @since n.e.x.t
* @access private
*/
-function ilo_print_detection_script() /*: void (in PHP 7.1) */ {
+function ilo_print_detection_script() {
if ( ! ilo_can_optimize_response() ) {
return;
}
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index 97902d4a30..298689243c 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -58,7 +58,7 @@ function ilo_get_page_metric_storage_lock_transient_key(): string {
* @since n.e.x.t
* @access private
*/
-function ilo_set_page_metric_storage_lock() /*: void (in PHP 7.1) */ {
+function ilo_set_page_metric_storage_lock() {
$ttl = ilo_get_page_metric_storage_lock_ttl();
$key = ilo_get_page_metric_storage_lock_transient_key();
if ( 0 === $ttl ) {
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index e712678d2b..bb7f4e9b75 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -20,7 +20,7 @@
* @since n.e.x.t
* @access private
*/
-function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
+function ilo_register_page_metrics_post_type() {
register_post_type(
ILO_PAGE_METRICS_POST_TYPE,
array(
@@ -49,7 +49,7 @@ function ilo_register_page_metrics_post_type() /*: void (in PHP 7.1) */ {
* @param string $slug Page metrics slug.
* @return WP_Post|null Post object if exists.
*/
-function ilo_get_page_metrics_post( string $slug ) /*: ?WP_Post (in PHP 7.1) */ {
+function ilo_get_page_metrics_post( string $slug ) {
$post_query = new WP_Query(
array(
'post_type' => ILO_PAGE_METRICS_POST_TYPE,
@@ -124,7 +124,7 @@ function ilo_parse_stored_page_metrics( WP_Post $post ): array {
* @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) /*: int|WP_Error (in PHP 8) */ {
+function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) {
$validated_page_metric['timestamp'] = microtime( true );
// TODO: What about storing a version identifier?
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 8cd9848c22..a9c68ef84d 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -20,7 +20,7 @@
* @since n.e.x.t
* @access private
*/
-function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
+function ilo_register_endpoint() {
$dom_rect_schema = array(
'type' => 'object',
@@ -160,7 +160,7 @@ function ilo_register_endpoint() /*: void (in PHP 7.1) */ {
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
-function ilo_handle_rest_request( WP_REST_Request $request ) /*: WP_REST_Response|WP_Error (in PHP 8) */ {
+function ilo_handle_rest_request( WP_REST_Request $request ) {
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) );
if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return new WP_Error(
From 9876aa44911a17fe5cae2843a34f5eb5c15368ef Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 12:03:32 -0800
Subject: [PATCH 086/371] Rename 'page metrics' to 'URL metrics'
---
.../image-loading-optimization/detection.php | 12 +-
.../detection/detect.js | 36 ++---
.../storage/data.php | 148 +++++++++---------
.../storage/lock.php | 26 +--
.../storage/post-type.php | 68 ++++----
.../storage/rest-api.php | 32 ++--
6 files changed, 161 insertions(+), 161 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection.php b/modules/images/image-loading-optimization/detection.php
index 90a1ffc461..c54229fc86 100644
--- a/modules/images/image-loading-optimization/detection.php
+++ b/modules/images/image-loading-optimization/detection.php
@@ -22,12 +22,12 @@ function ilo_print_detection_script() {
}
$query_vars = ilo_get_normalized_query_vars();
- $slug = ilo_get_page_metrics_slug( $query_vars );
+ $slug = ilo_get_url_metrics_slug( $query_vars );
$microtime = microtime( true );
// Abort if we already have all the sample size we need for all breakpoints.
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $slug );
- if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
+ if ( ! ilo_needs_url_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return;
}
@@ -50,12 +50,12 @@ function ilo_print_detection_script() {
'serveTime' => $microtime * 1000, // In milliseconds for comparison with `Date.now()` in JavaScript.
'detectionTimeWindow' => $detection_time_window,
'isDebug' => WP_DEBUG,
- 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_PAGE_METRICS_ROUTE ),
+ 'restApiEndpoint' => rest_url( ILO_REST_API_NAMESPACE . ILO_URL_METRICS_ROUTE ),
'restApiNonce' => wp_create_nonce( 'wp_rest' ),
- 'pageMetricsSlug' => $slug,
- 'pageMetricsNonce' => ilo_get_page_metrics_storage_nonce( $slug ),
+ 'urlMetricsSlug' => $slug,
+ 'urlMetricsNonce' => ilo_get_url_metrics_storage_nonce( $slug ),
'neededMinimumViewportWidths' => $needed_minimum_viewport_widths,
- 'storageLockTTL' => ilo_get_page_metric_storage_lock_ttl(),
+ 'storageLockTTL' => ilo_get_url_metric_storage_lock_ttl(),
);
wp_print_inline_script_tag(
sprintf(
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 96d0587a4b..512832e7f2 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -93,7 +93,7 @@ function error( ...message ) {
*/
/**
- * @typedef {Object} PageMetrics
+ * @typedef {Object} URLMetrics
* @property {string} url - URL of the page.
* @property {Object} viewport - Viewport.
* @property {number} viewport.width - Viewport width.
@@ -137,11 +137,11 @@ function getBreadcrumbs( leafElement ) {
}
/**
- * Checks whether the page metric(s) for the provided viewport width is needed.
+ * Checks whether the URL metric(s) for the provided viewport width is needed.
*
* @param {number} viewportWidth - Current viewport width.
* @param {Array[]} neededMinimumViewportWidths - Needed minimum viewport widths, in ascending order.
- * @return {boolean} Whether page metrics are needed.
+ * @return {boolean} Whether URL metrics are needed.
*/
function isViewportNeeded( viewportWidth, neededMinimumViewportWidths ) {
let lastWasNeeded = false;
@@ -176,10 +176,10 @@ function getCurrentTime() {
* @param {boolean} args.isDebug Whether to show debug messages.
* @param {string} args.restApiEndpoint URL for where to send the detection data.
* @param {string} args.restApiNonce Nonce for writing to the REST API.
- * @param {string} args.pageMetricsSlug Slug for page metrics.
- * @param {string} args.pageMetricsNonce Nonce for page metrics storage.
- * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for page metrics.
- * @param {number} args.storageLockTTL The TTL (in seconds) for the page metric storage lock.
+ * @param {string} args.urlMetricsSlug Slug for URL metrics.
+ * @param {string} args.urlMetricsNonce Nonce for URL metrics storage.
+ * @param {Array[]} args.neededMinimumViewportWidths Needed minimum viewport widths for URL metrics.
+ * @param {number} args.storageLockTTL The TTL (in seconds) for the URL metric storage lock.
*/
export default async function detect( {
serveTime,
@@ -187,15 +187,15 @@ export default async function detect( {
isDebug,
restApiEndpoint,
restApiNonce,
- pageMetricsSlug,
- pageMetricsNonce,
+ urlMetricsSlug,
+ urlMetricsNonce,
neededMinimumViewportWidths,
storageLockTTL,
} ) {
const currentTime = getCurrentTime();
// As an alternative to this, the ilo_print_detection_script() function can short-circuit if the
- // ilo_is_page_metric_storage_locked() function returns true. However, the downside with that is page caching could
+ // ilo_is_url_metric_storage_locked() function returns true. However, the downside with that is page caching could
// result in metrics being missed being gathered when a user navigates around a site and primes the page cache.
if ( isStorageLocked( currentTime, storageLockTTL ) ) {
if ( isDebug ) {
@@ -227,7 +227,7 @@ export default async function detect( {
if ( ! isViewportNeeded( win.innerWidth, neededMinimumViewportWidths ) ) {
if ( isDebug ) {
- log( 'No need for page metrics from the current viewport.' );
+ log( 'No need for URL metrics from the current viewport.' );
}
return;
}
@@ -354,11 +354,11 @@ export default async function detect( {
log( 'Detection is stopping.' );
}
- /** @type {PageMetrics} */
- const pageMetrics = {
+ /** @type {URLMetrics} */
+ const urlMetrics = {
url: win.location.href,
- slug: pageMetricsSlug,
- nonce: pageMetricsNonce,
+ slug: urlMetricsSlug,
+ nonce: urlMetricsNonce,
viewport: {
width: win.innerWidth,
height: win.innerHeight,
@@ -396,11 +396,11 @@ export default async function detect( {
boundingClientRect: elementIntersection.boundingClientRect,
};
- pageMetrics.elements.push( elementMetrics );
+ urlMetrics.elements.push( elementMetrics );
}
if ( isDebug ) {
- log( 'Page metrics:', pageMetrics );
+ log( 'URL metrics:', urlMetrics );
}
// TODO: Wait until idle? Yield to main?
@@ -411,7 +411,7 @@ export default async function detect( {
'Content-Type': 'application/json',
'X-WP-Nonce': restApiNonce,
},
- body: JSON.stringify( pageMetrics ),
+ body: JSON.stringify( urlMetrics ),
} );
if ( response.status === 200 ) {
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 211d1210f0..731275d3ea 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -11,24 +11,24 @@
}
/**
- * Gets the freshness age (TTL) for a given page metric.
+ * Gets the freshness age (TTL) for a given URL metric.
*
- * When a page metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint.
+ * When a URL metric expires it is eligible to be replaced by a newer one if its viewport lies within the same breakpoint.
*
* @since n.e.x.t
* @access private
*
* @return int Expiration TTL in seconds.
*/
-function ilo_get_page_metric_freshness_ttl(): int {
+function ilo_get_url_metric_freshness_ttl(): int {
/**
- * Filters the freshness age (TTL) for a given page metric.
+ * Filters the freshness age (TTL) for a given URL metric.
*
* @since n.e.x.t
*
* @param int $ttl Expiration TTL in seconds.
*/
- return (int) apply_filters( 'ilo_page_metric_freshness_ttl', DAY_IN_SECONDS );
+ return (int) apply_filters( 'ilo_url_metric_freshness_ttl', DAY_IN_SECONDS );
}
/**
@@ -59,7 +59,7 @@ function ilo_can_optimize_response(): bool {
/**
* Gets the normalized query vars for the current request.
*
- * This is used as a cache key for stored page metrics.
+ * This is used as a cache key for stored URL metrics.
*
* TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache.
*
@@ -86,7 +86,7 @@ function ilo_get_normalized_query_vars(): array {
}
/**
- * Gets slug for page metrics.
+ * Gets slug for URL metrics.
*
* @since n.e.x.t
* @access private
@@ -96,69 +96,69 @@ function ilo_get_normalized_query_vars(): array {
* @param array $query_vars Normalized query vars.
* @return string Slug.
*/
-function ilo_get_page_metrics_slug( array $query_vars ): string {
+function ilo_get_url_metrics_slug( array $query_vars ): string {
return md5( wp_json_encode( $query_vars ) );
}
/**
- * Computes nonce for storing page metrics for a specific slug.
+ * Computes nonce for storing URL metrics for a specific slug.
*
- * This is used in the REST API to authenticate the storage of new page metrics from a given URL.
+ * This is used in the REST API to authenticate the storage of new URL metrics from a given URL.
*
* @since n.e.x.t
* @access private
*
* @see wp_create_nonce()
- * @see ilo_verify_page_metrics_storage_nonce()
+ * @see ilo_verify_url_metrics_storage_nonce()
*
- * @param string $slug Page metrics slug.
+ * @param string $slug URL metrics slug.
* @return string Nonce.
*/
-function ilo_get_page_metrics_storage_nonce( string $slug ): string {
- return wp_create_nonce( "store_page_metrics:{$slug}" );
+function ilo_get_url_metrics_storage_nonce( string $slug ): string {
+ return wp_create_nonce( "store_url_metrics:{$slug}" );
}
/**
- * Verifies nonce for storing page metrics for a specific slug.
+ * Verifies nonce for storing URL metrics for a specific slug.
*
* @since n.e.x.t
* @access private
*
* @see wp_verify_nonce()
- * @see ilo_get_page_metrics_storage_nonce()
+ * @see ilo_get_url_metrics_storage_nonce()
*
- * @param string $nonce Page metrics storage nonce.
- * @param string $slug Page metrics slug.
+ * @param string $nonce URL metrics storage nonce.
+ * @param string $slug URL metrics slug.
* @return int 1 if the nonce is valid and generated between 0-12 hours ago,
* 2 if the nonce is valid and generated between 12-24 hours ago.
* 0 if the nonce is invalid.
*/
-function ilo_verify_page_metrics_storage_nonce( string $nonce, string $slug ): int {
- return (int) wp_verify_nonce( $nonce, "store_page_metrics:{$slug}" );
+function ilo_verify_url_metrics_storage_nonce( string $nonce, string $slug ): int {
+ return (int) wp_verify_nonce( $nonce, "store_url_metrics:{$slug}" );
}
/**
- * Unshifts a new page metric onto an array of page metrics.
+ * Unshifts a new URL metric onto an array of URL metrics.
*
* @since n.e.x.t
* @access private
*
- * @param array $page_metrics Page metrics.
- * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
- * @return array Updated page metrics.
+ * @param array $url_metrics URL metrics.
+ * @param array $validated_url_metric Validated URL metric. See JSON Schema defined in ilo_register_endpoint().
+ * @return array Updated URL metrics.
*/
-function ilo_unshift_page_metrics( array $page_metrics, array $validated_page_metric ): array {
- array_unshift( $page_metrics, $validated_page_metric );
- $breakpoints = ilo_get_breakpoint_max_widths();
- $sample_size = ilo_get_page_metrics_breakpoint_sample_size();
- $grouped_page_metrics = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoints );
+function ilo_unshift_url_metrics( array $url_metrics, array $validated_url_metric ): array {
+ array_unshift( $url_metrics, $validated_url_metric );
+ $breakpoints = ilo_get_breakpoint_max_widths();
+ $sample_size = ilo_get_url_metrics_breakpoint_sample_size();
+ $grouped_url_metrics = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoints );
- foreach ( $grouped_page_metrics as &$breakpoint_page_metrics ) {
- if ( count( $breakpoint_page_metrics ) > $sample_size ) {
+ foreach ( $grouped_url_metrics as &$breakpoint_url_metrics ) {
+ if ( count( $breakpoint_url_metrics ) > $sample_size ) {
- // Sort page metrics in descending order by timestamp.
+ // Sort URL metrics in descending order by timestamp.
usort(
- $breakpoint_page_metrics,
+ $breakpoint_url_metrics,
static function ( $a, $b ) {
if ( ! isset( $a['timestamp'] ) || ! isset( $b['timestamp'] ) ) {
return 0;
@@ -167,15 +167,15 @@ static function ( $a, $b ) {
}
);
- $breakpoint_page_metrics = array_slice( $breakpoint_page_metrics, 0, $sample_size );
+ $breakpoint_url_metrics = array_slice( $breakpoint_url_metrics, 0, $sample_size );
}
}
- return array_merge( ...$grouped_page_metrics );
+ return array_merge( ...$grouped_url_metrics );
}
/**
- * Gets the breakpoint max widths to group page metrics for various viewports.
+ * Gets the breakpoint max widths to group URL metrics for various viewports.
*
* Each max with represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then
* this means there will be two viewport groupings, one for 0<=480, and another >480. If instead there were three
@@ -198,7 +198,7 @@ static function ( $breakpoint_max_width ) {
return (int) $breakpoint_max_width;
},
/**
- * Filters the breakpoint max widths to group page metrics for various viewports.
+ * Filters the breakpoint max widths to group URL metrics for various viewports.
*
* @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
*/
@@ -210,42 +210,42 @@ static function ( $breakpoint_max_width ) {
}
/**
- * Gets the sample size for a breakpoint's page metrics on a given URL.
+ * Gets the sample size for a breakpoint's URL metrics on a given URL.
*
- * A breakpoint divides page metrics for viewports which are smaller and those which are larger. Given the default
+ * A breakpoint divides URL metrics for viewports which are smaller and those which are larger. Given the default
* sample size of 3 and there being just a single breakpoint (480) by default, for a given URL, there would be a maximum
- * total of 6 page metrics stored for a given URL: 3 for mobile and 3 for desktop.
+ * total of 6 URL metrics stored for a given URL: 3 for mobile and 3 for desktop.
*
* @since n.e.x.t
* @access private
*
* @return int Sample size.
*/
-function ilo_get_page_metrics_breakpoint_sample_size(): int {
+function ilo_get_url_metrics_breakpoint_sample_size(): int {
/**
- * Filters the sample size for a breakpoint's page metrics on a given URL.
+ * Filters the sample size for a breakpoint's URL metrics on a given URL.
*
* @since n.e.x.t
*
* @param int $sample_size Sample size.
*/
- return (int) apply_filters( 'ilo_page_metrics_breakpoint_sample_size', 3 );
+ return (int) apply_filters( 'ilo_url_metrics_breakpoint_sample_size', 3 );
}
/**
- * Groups page metrics by breakpoint.
+ * Groups URL metrics by breakpoint.
*
* @since n.e.x.t
* @access private
*
- * @param array $page_metrics Page metrics.
+ * @param array $url_metrics URL metrics.
* @param int[] $breakpoints Viewport breakpoint max widths, sorted in ascending order.
- * @return array Page metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within
+ * @return array URL metrics grouped by breakpoint. The array keys are the minimum widths for a viewport to lie within
* the breakpoint. The returned array is always one larger than the provided array of breakpoints, since
* the breakpoints reflect the max inclusive boundaries whereas the return value is the groups of page
* metrics with viewports on either side of the breakpoint boundaries.
*/
-function ilo_group_page_metrics_by_breakpoint( array $page_metrics, array $breakpoints ): array {
+function ilo_group_url_metrics_by_breakpoint( array $url_metrics, array $breakpoints ): array {
// Convert breakpoint max widths into viewport minimum widths.
$viewport_minimum_widths = array_map(
@@ -257,11 +257,11 @@ static function ( $breakpoint ) {
$grouped = array_fill_keys( array_merge( array( 0 ), $viewport_minimum_widths ), array() );
- foreach ( $page_metrics as $page_metric ) {
- if ( ! isset( $page_metric['viewport']['width'] ) ) {
+ foreach ( $url_metrics as $url_metric ) {
+ if ( ! isset( $url_metric['viewport']['width'] ) ) {
continue;
}
- $viewport_width = $page_metric['viewport']['width'];
+ $viewport_width = $url_metric['viewport']['width'];
$current_minimum_viewport = 0;
foreach ( $viewport_minimum_widths as $viewport_minimum_width ) {
@@ -272,7 +272,7 @@ static function ( $breakpoint ) {
}
}
- $grouped[ $current_minimum_viewport ][] = $page_metric;
+ $grouped[ $current_minimum_viewport ][] = $url_metric;
}
return $grouped;
}
@@ -283,31 +283,31 @@ static function ( $breakpoint ) {
* @since n.e.x.t
* @access private
*
- * @param array $page_metrics Page metrics.
+ * @param array $url_metrics URL metrics.
* @param float $current_time Current time as returned by microtime(true).
* @param int[] $breakpoint_max_widths Breakpoint max widths.
* @param int $sample_size Sample size for viewports in a breakpoint.
- * @param int $freshness_ttl Freshness TTL for a page metric.
- * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
+ * @param int $freshness_ttl Freshness TTL for a URL metric.
+ * @return array Array of tuples mapping minimum viewport width to whether URL metric(s) are needed.
*/
-function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array {
- $metrics_by_breakpoint = ilo_group_page_metrics_by_breakpoint( $page_metrics, $breakpoint_max_widths );
+function ilo_get_needed_minimum_viewport_widths( array $url_metrics, float $current_time, array $breakpoint_max_widths, int $sample_size, int $freshness_ttl ): array {
+ $metrics_by_breakpoint = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
$needed_minimum_viewport_widths = array();
- foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_page_metrics ) {
- $needs_page_metrics = false;
- if ( count( $viewport_page_metrics ) < $sample_size ) {
- $needs_page_metrics = true;
+ foreach ( $metrics_by_breakpoint as $minimum_viewport_width => $viewport_url_metrics ) {
+ $needs_url_metrics = false;
+ if ( count( $viewport_url_metrics ) < $sample_size ) {
+ $needs_url_metrics = true;
} else {
- foreach ( $viewport_page_metrics as $page_metric ) {
- if ( isset( $page_metric['timestamp'] ) && $page_metric['timestamp'] + $freshness_ttl < $current_time ) {
- $needs_page_metrics = true;
+ foreach ( $viewport_url_metrics as $url_metric ) {
+ if ( isset( $url_metric['timestamp'] ) && $url_metric['timestamp'] + $freshness_ttl < $current_time ) {
+ $needs_url_metrics = true;
break;
}
}
}
$needed_minimum_viewport_widths[] = array(
$minimum_viewport_width,
- $needs_page_metrics,
+ $needs_url_metrics,
);
}
@@ -324,30 +324,30 @@ function ilo_get_needed_minimum_viewport_widths( array $page_metrics, float $cur
*
* @see ilo_get_needed_minimum_viewport_widths()
*
- * @param string $slug Page metrics slug.
- * @return array Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
+ * @param string $slug URL metrics slug.
+ * @return array Array of tuples mapping minimum viewport width to whether URL metric(s) are needed.
*/
function ilo_get_needed_minimum_viewport_widths_now_for_slug( string $slug ): array {
- $post = ilo_get_page_metrics_post( $slug );
+ $post = ilo_get_url_metrics_post( $slug );
return ilo_get_needed_minimum_viewport_widths(
- $post instanceof WP_Post ? ilo_parse_stored_page_metrics( $post ) : array(),
+ $post instanceof WP_Post ? ilo_parse_stored_url_metrics( $post ) : array(),
microtime( true ),
ilo_get_breakpoint_max_widths(),
- ilo_get_page_metrics_breakpoint_sample_size(),
- ilo_get_page_metric_freshness_ttl()
+ ilo_get_url_metrics_breakpoint_sample_size(),
+ ilo_get_url_metric_freshness_ttl()
);
}
/**
- * Checks whether there is a page metric needed for one of the breakpoints.
+ * Checks whether there is a URL metric needed for one of the breakpoints.
*
* @since n.e.x.t
* @access private
*
- * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether page metric(s) are needed.
- * @return bool Whether a page metric is needed.
+ * @param array $needed_minimum_viewport_widths Array of tuples mapping minimum viewport width to whether URL metric(s) are needed.
+ * @return bool Whether a URL metric is needed.
*/
-function ilo_needs_page_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool {
+function ilo_needs_url_metric_for_breakpoint( array $needed_minimum_viewport_widths ): bool {
foreach ( $needed_minimum_viewport_widths as list( $minimum_viewport_width, $is_needed ) ) {
if ( $is_needed ) {
return true;
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index 298689243c..d30fe8d75c 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -11,14 +11,14 @@
}
/**
- * Gets the TTL (in seconds) for the page metric storage lock.
+ * Gets the TTL (in seconds) for the URL metric storage lock.
*
* @since n.e.x.t
* @access private
*
* @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used.
*/
-function ilo_get_page_metric_storage_lock_ttl(): int {
+function ilo_get_url_metric_storage_lock_ttl(): int {
/**
* Filters how long a given IP is locked from submitting another metric-storage REST API request.
@@ -39,18 +39,18 @@ function ilo_get_page_metric_storage_lock_ttl(): int {
}
/**
- * Gets transient key for locking page metric storage (for the current IP).
+ * Gets transient key for locking URL metric storage (for the current IP).
*
* @todo Should the URL be included in the key? Or should a user only be allowed to store one metric?
* @return string Transient key.
*/
-function ilo_get_page_metric_storage_lock_transient_key(): string {
+function ilo_get_url_metric_storage_lock_transient_key(): string {
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
- return 'page_metrics_storage_lock_' . wp_hash( $ip_address );
+ return 'url_metrics_storage_lock_' . wp_hash( $ip_address );
}
/**
- * Sets page metric storage lock (for the current IP).
+ * Sets URL metric storage lock (for the current IP).
*
* If the storage lock TTL is greater than zero, then a transient is set with the current timestamp and expiring at TTL
* seconds. Otherwise, if the current TTL is zero, then any transient is deleted.
@@ -58,9 +58,9 @@ function ilo_get_page_metric_storage_lock_transient_key(): string {
* @since n.e.x.t
* @access private
*/
-function ilo_set_page_metric_storage_lock() {
- $ttl = ilo_get_page_metric_storage_lock_ttl();
- $key = ilo_get_page_metric_storage_lock_transient_key();
+function ilo_set_url_metric_storage_lock() {
+ $ttl = ilo_get_url_metric_storage_lock_ttl();
+ $key = ilo_get_url_metric_storage_lock_transient_key();
if ( 0 === $ttl ) {
delete_transient( $key );
} else {
@@ -69,19 +69,19 @@ function ilo_set_page_metric_storage_lock() {
}
/**
- * Checks whether page metric storage is locked (for the current IP).
+ * Checks whether URL metric storage is locked (for the current IP).
*
* @since n.e.x.t
* @access private
*
* @return bool Whether locked.
*/
-function ilo_is_page_metric_storage_locked(): bool {
- $ttl = ilo_get_page_metric_storage_lock_ttl();
+function ilo_is_url_metric_storage_locked(): bool {
+ $ttl = ilo_get_url_metric_storage_lock_ttl();
if ( 0 === $ttl ) {
return false;
}
- $locked_time = get_transient( ilo_get_page_metric_storage_lock_transient_key() );
+ $locked_time = get_transient( ilo_get_url_metric_storage_lock_transient_key() );
if ( false === $locked_time ) {
return false;
}
diff --git a/modules/images/image-loading-optimization/storage/post-type.php b/modules/images/image-loading-optimization/storage/post-type.php
index bb7f4e9b75..a95a71176d 100644
--- a/modules/images/image-loading-optimization/storage/post-type.php
+++ b/modules/images/image-loading-optimization/storage/post-type.php
@@ -10,23 +10,23 @@
exit; // Exit if accessed directly.
}
-const ILO_PAGE_METRICS_POST_TYPE = 'ilo_page_metrics';
+const ILO_URL_METRICS_POST_TYPE = 'ilo_url_metrics';
/**
- * Registers post type for page metrics storage.
+ * Registers post type for URL metrics storage.
*
* This the configuration for this post type is similar to the oembed_cache in core.
*
* @since n.e.x.t
* @access private
*/
-function ilo_register_page_metrics_post_type() {
+function ilo_register_url_metrics_post_type() {
register_post_type(
- ILO_PAGE_METRICS_POST_TYPE,
+ ILO_URL_METRICS_POST_TYPE,
array(
'labels' => array(
- 'name' => __( 'Page Metrics', 'performance-lab' ),
- 'singular_name' => __( 'Page Metrics', 'performance-lab' ),
+ 'name' => __( 'URL Metrics', 'performance-lab' ),
+ 'singular_name' => __( 'URL Metrics', 'performance-lab' ),
),
'public' => false,
'hierarchical' => false,
@@ -38,21 +38,21 @@ function ilo_register_page_metrics_post_type() {
)
);
}
-add_action( 'init', 'ilo_register_page_metrics_post_type' );
+add_action( 'init', 'ilo_register_url_metrics_post_type' );
/**
- * Gets page metrics post.
+ * Gets URL metrics post.
*
* @since n.e.x.t
* @access private
*
- * @param string $slug Page metrics slug.
+ * @param string $slug URL metrics slug.
* @return WP_Post|null Post object if exists.
*/
-function ilo_get_page_metrics_post( string $slug ) {
+function ilo_get_url_metrics_post( string $slug ) {
$post_query = new WP_Query(
array(
- 'post_type' => ILO_PAGE_METRICS_POST_TYPE,
+ 'post_type' => ILO_URL_METRICS_POST_TYPE,
'post_status' => 'publish',
'name' => $slug,
'posts_per_page' => 1,
@@ -73,15 +73,15 @@ function ilo_get_page_metrics_post( string $slug ) {
}
/**
- * Parses post content in page metrics post.
+ * Parses post content in URL metrics post.
*
* @since n.e.x.t
* @access private
*
- * @param WP_Post $post Page metrics post.
- * @return array Page metrics.
+ * @param WP_Post $post URL metrics post.
+ * @return array URL metrics.
*/
-function ilo_parse_stored_page_metrics( WP_Post $post ): array {
+function ilo_parse_stored_url_metrics( WP_Post $post ): array {
$this_function = __FUNCTION__;
$trigger_error = static function ( $error ) use ( $this_function ) {
if ( function_exists( 'wp_trigger_error' ) ) {
@@ -89,63 +89,63 @@ function ilo_parse_stored_page_metrics( WP_Post $post ): array {
}
};
- $page_metrics = json_decode( $post->post_content, true );
+ $url_metrics = json_decode( $post->post_content, true );
if ( json_last_error() ) {
$trigger_error(
sprintf(
/* translators: 1: Post type slug, 2: JSON error message */
__( 'Contents of %1$s post type not valid JSON: %2$s', 'performance-lab' ),
- ILO_PAGE_METRICS_POST_TYPE,
+ ILO_URL_METRICS_POST_TYPE,
json_last_error_msg()
)
);
- $page_metrics = array();
- } elseif ( ! is_array( $page_metrics ) ) {
+ $url_metrics = array();
+ } elseif ( ! is_array( $url_metrics ) ) {
$trigger_error(
sprintf(
/* translators: %s is post type slug */
__( 'Contents of %s post type was not a JSON array.', 'performance-lab' ),
- ILO_PAGE_METRICS_POST_TYPE
+ ILO_URL_METRICS_POST_TYPE
)
);
- $page_metrics = array();
+ $url_metrics = array();
}
- return $page_metrics;
+ return $url_metrics;
}
/**
- * Stores page metric by merging it with the other page metrics for a given URL.
+ * Stores URL metric by merging it with the other URL metrics for a given URL.
*
* @since n.e.x.t
* @access private
*
- * @param string $url URL for the page metrics. This is used purely as metadata.
- * @param string $slug Page metrics slug (computed from query vars).
- * @param array $validated_page_metric Validated page metric. See JSON Schema defined in ilo_register_endpoint().
+ * @param string $url URL for the URL metrics. This is used purely as metadata.
+ * @param string $slug URL metrics slug (computed from query vars).
+ * @param array $validated_url_metric Validated URL metric. See JSON Schema defined in ilo_register_endpoint().
* @return int|WP_Error Post ID or WP_Error otherwise.
*/
-function ilo_store_page_metric( string $url, string $slug, array $validated_page_metric ) {
- $validated_page_metric['timestamp'] = microtime( true );
+function ilo_store_url_metric( string $url, string $slug, array $validated_url_metric ) {
+ $validated_url_metric['timestamp'] = microtime( true );
// TODO: What about storing a version identifier?
$post_data = array(
'post_title' => $url, // TODO: Should we keep this? It can help with debugging.
);
- $post = ilo_get_page_metrics_post( $slug );
+ $post = ilo_get_url_metrics_post( $slug );
if ( $post instanceof WP_Post ) {
$post_data['ID'] = $post->ID;
$post_data['post_name'] = $post->post_name;
- $page_metrics = ilo_parse_stored_page_metrics( $post );
+ $url_metrics = ilo_parse_stored_url_metrics( $post );
} else {
$post_data['post_name'] = $slug;
- $page_metrics = array();
+ $url_metrics = array();
}
- $page_metrics = ilo_unshift_page_metrics( $page_metrics, $validated_page_metric );
+ $url_metrics = ilo_unshift_url_metrics( $url_metrics, $validated_url_metric );
- $post_data['post_content'] = wp_json_encode( $page_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
+ $post_data['post_content'] = wp_json_encode( $url_metrics, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); // TODO: No need for pretty-printing.
$has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
if ( $has_kses ) {
@@ -153,7 +153,7 @@ function ilo_store_page_metric( string $url, string $slug, array $validated_page
kses_remove_filters();
}
- $post_data['post_type'] = ILO_PAGE_METRICS_POST_TYPE;
+ $post_data['post_type'] = ILO_URL_METRICS_POST_TYPE;
$post_data['post_status'] = 'publish';
if ( isset( $post_data['ID'] ) ) {
$result = wp_update_post( wp_slash( $post_data ), true );
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index a9c68ef84d..423355f344 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -12,10 +12,10 @@
const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1';
-const ILO_PAGE_METRICS_ROUTE = '/page-metrics';
+const ILO_URL_METRICS_ROUTE = '/url-metrics';
/**
- * Registers endpoint for storage of page metric.
+ * Registers endpoint for storage of URL metric.
*
* @since n.e.x.t
* @access private
@@ -39,7 +39,7 @@ function ilo_register_endpoint() {
register_rest_route(
ILO_REST_API_NAMESPACE,
- ILO_PAGE_METRICS_ROUTE,
+ ILO_URL_METRICS_ROUTE,
array(
'methods' => 'POST',
'callback' => static function ( WP_REST_Request $request ) {
@@ -47,10 +47,10 @@ function ilo_register_endpoint() {
},
'permission_callback' => static function () {
// Needs to be available to unauthenticated visitors.
- if ( ilo_is_page_metric_storage_locked() ) {
+ if ( ilo_is_url_metric_storage_locked() ) {
return new WP_Error(
- 'page_metric_storage_locked',
- __( 'Page metric storage is presently locked for the current IP.', 'performance-lab' ),
+ 'url_metric_storage_locked',
+ __( 'URL metric storage is presently locked for the current IP.', 'performance-lab' ),
array( 'status' => 403 )
);
}
@@ -78,8 +78,8 @@ function ilo_register_endpoint() {
'required' => true,
'pattern' => '^[0-9a-f]+$',
'validate_callback' => static function ( $nonce, WP_REST_Request $request ) {
- if ( ! ilo_verify_page_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) {
- return new WP_Error( 'invalid_nonce', __( 'Page metrics nonce verification failure.', 'performance-lab' ) );
+ if ( ! ilo_verify_url_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ) ) ) {
+ return new WP_Error( 'invalid_nonce', __( 'URL metrics nonce verification failure.', 'performance-lab' ) );
}
return true;
},
@@ -162,21 +162,21 @@ function ilo_register_endpoint() {
*/
function ilo_handle_rest_request( WP_REST_Request $request ) {
$needed_minimum_viewport_widths = ilo_get_needed_minimum_viewport_widths_now_for_slug( $request->get_param( 'slug' ) );
- if ( ! ilo_needs_page_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
+ if ( ! ilo_needs_url_metric_for_breakpoint( $needed_minimum_viewport_widths ) ) {
return new WP_Error(
- 'no_page_metric_needed',
- __( 'No page metric needed for any of the breakpoints.', 'performance-lab' ),
+ 'no_url_metric_needed',
+ __( 'No URL metric needed for any of the breakpoints.', 'performance-lab' ),
array( 'status' => 403 )
);
}
- ilo_set_page_metric_storage_lock();
- $new_page_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) );
+ ilo_set_url_metric_storage_lock();
+ $new_url_metric = wp_array_slice_assoc( $request->get_json_params(), array( 'viewport', 'elements' ) );
- $result = ilo_store_page_metric(
+ $result = ilo_store_url_metric(
$request->get_param( 'url' ),
$request->get_param( 'slug' ),
- $new_page_metric
+ $new_url_metric
);
if ( $result instanceof WP_Error ) {
@@ -187,7 +187,7 @@ function ilo_handle_rest_request( WP_REST_Request $request ) {
array(
'success' => true,
'post_id' => $result,
- 'data' => ilo_parse_stored_page_metrics( ilo_get_page_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data.
+ 'data' => ilo_parse_stored_url_metrics( ilo_get_url_metrics_post( $request->get_param( 'slug' ) ) ), // TODO: Remove this debug data.
)
);
}
From 82b6210d897dfec56dbe6d07abf30a023e454d11 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 12:04:12 -0800
Subject: [PATCH 087/371] Remove unnecessary curly braces
---
modules/images/image-loading-optimization/storage/data.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 731275d3ea..0f091a324b 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -115,7 +115,7 @@ function ilo_get_url_metrics_slug( array $query_vars ): string {
* @return string Nonce.
*/
function ilo_get_url_metrics_storage_nonce( string $slug ): string {
- return wp_create_nonce( "store_url_metrics:{$slug}" );
+ return wp_create_nonce( "store_url_metrics:$slug" );
}
/**
@@ -134,7 +134,7 @@ function ilo_get_url_metrics_storage_nonce( string $slug ): string {
* 0 if the nonce is invalid.
*/
function ilo_verify_url_metrics_storage_nonce( string $nonce, string $slug ): int {
- return (int) wp_verify_nonce( $nonce, "store_url_metrics:{$slug}" );
+ return (int) wp_verify_nonce( $nonce, "store_url_metrics:$slug" );
}
/**
From 1168a5a4b383be56b03398492dc5773e669e54c2 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 12:06:50 -0800
Subject: [PATCH 088/371] Rename filter to ilo_url_metric_storage_lock_ttl
---
modules/images/image-loading-optimization/storage/lock.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/lock.php b/modules/images/image-loading-optimization/storage/lock.php
index d30fe8d75c..2d8992e31f 100644
--- a/modules/images/image-loading-optimization/storage/lock.php
+++ b/modules/images/image-loading-optimization/storage/lock.php
@@ -34,7 +34,7 @@ function ilo_get_url_metric_storage_lock_ttl(): int {
*
* @param int $ttl TTL.
*/
- $ttl = (int) apply_filters( 'ilo_metrics_storage_lock_ttl', MINUTE_IN_SECONDS );
+ $ttl = (int) apply_filters( 'ilo_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS );
return max( 0, $ttl );
}
From 698d89f6b8ce3a0cb90eec3f35eced3b4c22b7ca Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 12:57:22 -0800
Subject: [PATCH 089/371] Follow AIP-136
---
.../storage/rest-api.php | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index 423355f344..aaa7c98500 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -10,9 +10,24 @@
exit; // Exit if accessed directly.
}
+/**
+ * Namespace for image-loading-optimization.
+ *
+ * @var string
+ */
const ILO_REST_API_NAMESPACE = 'image-loading-optimization/v1';
-const ILO_URL_METRICS_ROUTE = '/url-metrics';
+/**
+ * Route for storing a URL metric.
+ *
+ * Note the `:store` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way
+ * that does not strictly follow the standard usage. Namely, submitting a POST request to this endpoint will either
+ * create a new `ilo_url_metrics` post, or it will update an existing post if one already exists for the provided slug.
+ *
+ * @link https://google.aip.dev/136
+ * @var string
+ */
+const ILO_URL_METRICS_ROUTE = '/url-metrics:store';
/**
* Registers endpoint for storage of URL metric.
From 47580138d95f9d1e110f6f922ddc397bc473fecb Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 15 Nov 2023 10:17:57 -0800
Subject: [PATCH 090/371] Fix prefix for output buffer template filter
---
modules/images/image-loading-optimization/hooks.php | 2 +-
server-timing/class-perflab-server-timing.php | 2 +-
tests/modules/images/image-loading-optimization/load-tests.php | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/hooks.php b/modules/images/image-loading-optimization/hooks.php
index 426f20b646..096588a94f 100644
--- a/modules/images/image-loading-optimization/hooks.php
+++ b/modules/images/image-loading-optimization/hooks.php
@@ -41,7 +41,7 @@ static function ( string $output ): string {
* @param string $output Output buffer.
* @return string Filtered output buffer.
*/
- return (string) apply_filters( 'perflab_template_output_buffer', $output );
+ return (string) apply_filters( 'ilo_template_output_buffer', $output );
}
);
return $passthrough;
diff --git a/server-timing/class-perflab-server-timing.php b/server-timing/class-perflab-server-timing.php
index aeb442a764..c718e8f60e 100644
--- a/server-timing/class-perflab-server-timing.php
+++ b/server-timing/class-perflab-server-timing.php
@@ -230,7 +230,7 @@ public function on_template_include( $passthrough = null ) {
// It feels better if this could rather be replaced with add_action( 'shutdown', [ $this, 'send_header' ] )
// However, this does not work because the buffer is sent before the shutdown callback is executed.
add_filter(
- 'perflab_template_output_buffer',
+ 'ilo_template_output_buffer',
function ( $buffer ) {
$this->send_header();
return $buffer;
diff --git a/tests/modules/images/image-loading-optimization/load-tests.php b/tests/modules/images/image-loading-optimization/load-tests.php
index 1015d12b7d..661d876f8c 100644
--- a/tests/modules/images/image-loading-optimization/load-tests.php
+++ b/tests/modules/images/image-loading-optimization/load-tests.php
@@ -34,7 +34,7 @@ public function it_buffers_and_filters_output() {
ob_start();
add_filter(
- 'perflab_template_output_buffer',
+ 'ilo_template_output_buffer',
function ( $buffer ) use ( $original, $expected ) {
$this->assertSame( $original, $buffer );
return $expected;
From 5fb6fda4cae0b9e9e998ae6d007d6378bf7408e0 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 16 Nov 2023 14:11:13 -0800
Subject: [PATCH 091/371] Add small and medium breakpoints from Gutenberg to go
along with mobile
---
.../image-loading-optimization/storage/data.php | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 0f091a324b..4603abadea 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -186,8 +186,17 @@ static function ( $a, $b ) {
* 3. 481-576 (phablets)
* 4. >576 (desktop)
*
+ * The default breakpoints are reused from Gutenberg where the _breakpoints.scss file includes these variables:
+ *
+ * $break-medium: 782px; // adminbar goes big
+ * $break-small: 600px;
+ * $break-mobile: 480px;
+ *
+ * These breakpoints appear to be used the most in media queries that affect frontend styles.
+ *
* @since n.e.x.t
* @access private
+ * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13
*
* @return int[] Breakpoint max widths, sorted in ascending order.
*/
@@ -200,9 +209,11 @@ static function ( $breakpoint_max_width ) {
/**
* Filters the breakpoint max widths to group URL metrics for various viewports.
*
+ * @since n.e.x.t
+ *
* @param int[] $breakpoint_max_widths Max widths for viewport breakpoints.
*/
- (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480 ) )
+ (array) apply_filters( 'ilo_breakpoint_max_widths', array( 480, 600, 782 ) )
);
sort( $breakpoint_max_widths );
From bd1153d88d5305c2251ca08b7c1999cb0060b7fe Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 16 Nov 2023 14:30:42 -0800
Subject: [PATCH 092/371] Add ilo_get_lcp_elements_by_minimum_viewport_widths()
---
.../storage/data.php | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 4603abadea..875e304da0 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -288,6 +288,66 @@ static function ( $breakpoint ) {
return $grouped;
}
+/**
+ * Gets the LCP element for each breakpoint.
+ *
+ * The array keys are the minimum viewport width required for the element to be LCP.
+ *
+ * @param array $url_metrics URL metrics.
+ * @param int[] $breakpoint_max_widths Breakpoint max widths.
+ * @return array LCP elements keyed by its minimum viewport width.
+ */
+function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, array $breakpoint_max_widths ): array {
+ $grouped_url_metrics = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
+
+ $lcp_element_by_viewport_minimum_width = array();
+ foreach ( $grouped_url_metrics as $viewport_minimum_width => $breakpoint_url_metrics ) {
+
+ // The following arrays all share array indices.
+ $seen_breadcrumbs = array();
+ $breadcrumb_counts = array();
+ $breadcrumb_element = array();
+
+ foreach ( $breakpoint_url_metrics as $breakpoint_url_metric ) {
+ foreach ( $breakpoint_url_metric['elements'] as $element ) {
+ if ( ! $element['isLCP'] ) {
+ continue;
+ }
+
+ $i = array_search( $element['breadcrumbs'], $seen_breadcrumbs, true );
+ if ( false === $i ) {
+ $i = count( $seen_breadcrumbs );
+ $seen_breadcrumbs[ $i ] = $element['breadcrumbs'];
+ $breadcrumb_counts[ $i ] = 0;
+ }
+
+ $breadcrumb_counts[ $i ] += 1;
+ $breadcrumb_element[ $i ] = $element;
+ break; // We found the LCP element for the URL metric, go to the next URL metric.
+ }
+ }
+
+ // Now sort by the breadcrumb counts in descending order, so the remaining first key is the most common breadcrumb.
+ if ( $seen_breadcrumbs ) {
+ arsort( $breadcrumb_counts );
+ $most_common_breadcrumb_index = key( $breadcrumb_counts );
+
+ $lcp_element_by_viewport_minimum_width[ $viewport_minimum_width ] = $breadcrumb_element[ $most_common_breadcrumb_index ];
+ }
+ }
+
+ // Now we need to merge the breakpoints when there is an LCP element common between them.
+ $reduced_breadcrumbs = array();
+ $last_breadcrumb_element = null;
+ foreach ( $lcp_element_by_viewport_minimum_width as $viewport_minimum_width => $lcp_element ) {
+ if ( ! $last_breadcrumb_element || $lcp_element['breadcrumbs'] !== $last_breadcrumb_element['breadcrumbs'] ) {
+ $reduced_breadcrumbs[ $viewport_minimum_width ] = $lcp_element;
+ $last_breadcrumb_element = $lcp_element;
+ }
+ }
+ return $reduced_breadcrumbs;
+}
+
/**
* Gets needed minimum viewport widths.
*
From a990982e67095c65feecf720e913a0a59cdf5721 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 16 Nov 2023 14:37:35 -0800
Subject: [PATCH 093/371] Use array_filter() to simplify breakpoint merge
---
.../image-loading-optimization/storage/data.php | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 875e304da0..f37b75daeb 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -337,15 +337,15 @@ function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, ar
}
// Now we need to merge the breakpoints when there is an LCP element common between them.
- $reduced_breadcrumbs = array();
- $last_breadcrumb_element = null;
- foreach ( $lcp_element_by_viewport_minimum_width as $viewport_minimum_width => $lcp_element ) {
- if ( ! $last_breadcrumb_element || $lcp_element['breadcrumbs'] !== $last_breadcrumb_element['breadcrumbs'] ) {
- $reduced_breadcrumbs[ $viewport_minimum_width ] = $lcp_element;
- $last_breadcrumb_element = $lcp_element;
+ $last_lcp_element = null;
+ return array_filter(
+ $lcp_element_by_viewport_minimum_width,
+ static function ( $lcp_element ) use ( &$last_lcp_element ) {
+ $include = ( ! $last_lcp_element || $last_lcp_element['breadcrumbs'] !== $lcp_element['breadcrumbs'] );
+ $last_lcp_element = $lcp_element;
+ return $include;
}
- }
- return $reduced_breadcrumbs;
+ );
}
/**
From b18a0c7aba7861030b5f891d31cb18ec951c28e9 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 16 Nov 2023 16:03:03 -0800
Subject: [PATCH 094/371] Remove fetchpriority from images when different
breakpoints have different LCP images
---
.../image-loading-optimization/load.php | 2 +
.../optimization.php | 51 +++++++++++++++++++
.../storage/data.php | 2 +
3 files changed, 55 insertions(+)
create mode 100644 modules/images/image-loading-optimization/optimization.php
diff --git a/modules/images/image-loading-optimization/load.php b/modules/images/image-loading-optimization/load.php
index 1086e0ed9d..b253f9d8b3 100644
--- a/modules/images/image-loading-optimization/load.php
+++ b/modules/images/image-loading-optimization/load.php
@@ -24,3 +24,5 @@
require_once __DIR__ . '/storage/rest-api.php';
require_once __DIR__ . '/detection.php';
+
+require_once __DIR__ . '/optimization.php';
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
new file mode 100644
index 0000000000..70a19005ef
--- /dev/null
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -0,0 +1,51 @@
+next_tag( array( 'tag_name' => 'IMG' ) ) ) {
+ if ( $p->get_attribute( 'fetchpriority' ) ) {
+ $p->set_attribute( 'data-wp-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
+ $p->remove_attribute( 'fetchpriority' );
+ }
+ }
+ $buffer = $p->get_updated_html();
+ }
+ }
+
+ return $buffer;
+}
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index f37b75daeb..7648828d9f 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -293,6 +293,8 @@ static function ( $breakpoint ) {
*
* The array keys are the minimum viewport width required for the element to be LCP.
*
+ * @TODO: If there is no LCP element at a given breakpoint, make sure to return null?
+ *
* @param array $url_metrics URL metrics.
* @param int[] $breakpoint_max_widths Breakpoint max widths.
* @return array LCP elements keyed by its minimum viewport width.
From 25ea308628ed306309b48569b67b6eb328bb623d Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 17 Nov 2023 11:15:40 -0800
Subject: [PATCH 095/371] Update
ilo_get_lcp_elements_by_minimum_viewport_widths() to account for URL metrics
with no LCP element
---
.../storage/data.php | 26 ++++++++++++++++---
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 7648828d9f..8d529cc0f5 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -291,9 +291,10 @@ static function ( $breakpoint ) {
/**
* Gets the LCP element for each breakpoint.
*
- * The array keys are the minimum viewport width required for the element to be LCP.
- *
- * @TODO: If there is no LCP element at a given breakpoint, make sure to return null?
+ * The array keys are the minimum viewport width required for the element to be LCP. If there are URL metrics for a
+ * given breakpoint and yet there is no LCP element, then the array value is `false`. If there is an LCP element at the
+ * breakpoint, then the array value is an array representing that element, including its breadcrumbs. If two adjoining
+ * breakpoints have the same value, then the latter is dropped.
*
* @param array $url_metrics URL metrics.
* @param int[] $breakpoint_max_widths Breakpoint max widths.
@@ -335,6 +336,8 @@ function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, ar
$most_common_breadcrumb_index = key( $breadcrumb_counts );
$lcp_element_by_viewport_minimum_width[ $viewport_minimum_width ] = $breadcrumb_element[ $most_common_breadcrumb_index ];
+ } elseif ( ! empty( $breakpoint_url_metrics ) ) {
+ $lcp_element_by_viewport_minimum_width[ $viewport_minimum_width ] = false; // No LCP image at this breakpoint.
}
}
@@ -343,7 +346,22 @@ function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, ar
return array_filter(
$lcp_element_by_viewport_minimum_width,
static function ( $lcp_element ) use ( &$last_lcp_element ) {
- $include = ( ! $last_lcp_element || $last_lcp_element['breadcrumbs'] !== $lcp_element['breadcrumbs'] );
+ $include = (
+ // First element in list.
+ null === $last_lcp_element
+ ||
+ ( is_array( $last_lcp_element ) && is_array( $lcp_element )
+ ?
+ // This breakpoint and previous breakpoint had LCP element, and they were not the same element.
+ $last_lcp_element['breadcrumbs'] !== $lcp_element['breadcrumbs']
+ :
+ // This LCP element and the last LCP element were not the same. In this case, either variable may be
+ // false or an array, but both cannot be an array. If both are false, we don't want to include since
+ // it is the same. If one is an array and the other is false, then do want to include because this
+ // indicates a difference at this breakpoint.
+ $last_lcp_element !== $lcp_element
+ )
+ );
$last_lcp_element = $lcp_element;
return $include;
}
From c88f358d567986611a122f1ad245d87b0cbb2ade Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 17 Nov 2023 16:41:26 -0800
Subject: [PATCH 096/371] WIP: Breadcrumb calculation on server
---
.../detection/detect.js | 11 +-
.../optimization.php | 217 ++++++++++++++++--
.../storage/rest-api.php | 2 +-
3 files changed, 214 insertions(+), 16 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 512832e7f2..431ade5a24 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -7,6 +7,8 @@ const consoleLogPrefix = '[Image Loading Optimization]';
const storageLockTimeSessionKey = 'iloStorageLockTime';
+const adminBarId = 'wpadminbar';
+
/**
* Checks whether storage is locked.
*
@@ -111,7 +113,12 @@ function getElementIndex( element ) {
if ( ! element.parentElement ) {
return 0;
}
- return [ ...element.parentElement.children ].indexOf( element );
+ const children = [ ...element.parentElement.children ];
+ let index = children.indexOf( element );
+ if ( children.includes( document.getElementById( adminBarId ) ) ) {
+ --index;
+ }
+ return index;
}
/**
@@ -238,7 +245,7 @@ export default async function detect( {
// Obtain the admin bar element because we don't want to detect elements inside of it.
const adminBar =
- /** @type {?HTMLDivElement} */ doc.getElementById( 'wpadminbar' );
+ /** @type {?HTMLDivElement} */ doc.getElementById( adminBarId );
// We need to capture the original elements and their breadcrumbs as early as possible in case JavaScript is
// mutating the DOM from the original HTML rendered by the server, in which case the breadcrumbs obtained from the
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 70a19005ef..8934daf833 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -10,6 +10,70 @@
exit; // Exit if accessed directly.
}
+/**
+ * HTML elements that are self-closing.
+ *
+ * @link https://www.w3.org/TR/html5/syntax.html#serializing-html-fragments
+ * @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L206-L232
+ *
+ * @var string[]
+ */
+const ILO_SELF_CLOSING_TAGS = array(
+ 'AREA',
+ 'BASE',
+ 'BASEFONT',
+ 'BGSOUND',
+ 'BR',
+ 'COL',
+ 'EMBED',
+ 'FRAME',
+ 'HR',
+ 'IMG',
+ 'INPUT',
+ 'KEYGEN',
+ 'LINK',
+ 'META',
+ 'PARAM',
+ 'SOURCE',
+ 'TRACK',
+ 'WBR',
+);
+
+/**
+ * The set of HTML tags whose presence will implicitly close a element.
+ * For example '
foo
bar ' should parse the same as 'foo
bar '.
+ *
+ * @link https://www.w3.org/TR/html-markup/p.html
+ * @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L262-L293
+ */
+const ILO_P_CLOSING_TAGS = array(
+ 'ADDRESS',
+ 'ARTICLE',
+ 'ASIDE',
+ 'BLOCKQUOTE',
+ 'DIR',
+ 'DL',
+ 'FIELDSET',
+ 'FOOTER',
+ 'FORM',
+ 'H1',
+ 'H2',
+ 'H3',
+ 'H4',
+ 'H5',
+ 'H6',
+ 'HEADER',
+ 'HR',
+ 'MENU',
+ 'NAV',
+ 'OL',
+ 'P',
+ 'PRE',
+ 'SECTION',
+ 'TABLE',
+ 'UL',
+);
+
/**
* Adds template output buffer filter for optimization if eligible.
*/
@@ -21,6 +85,121 @@ function ilo_maybe_add_template_output_buffer_filter() {
}
add_action( 'wp', 'ilo_maybe_add_template_output_buffer_filter' );
+function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widths ): string {
+ $minimum_viewport_widths = array_keys( $lcp_images_by_minimum_viewport_widths );
+ for ( $i = 0, $len = count( $minimum_viewport_widths ); $i < $len; $i++ ) {
+ $lcp_element = $lcp_images_by_minimum_viewport_widths[ $minimum_viewport_widths[ $i ] ];
+ if ( false === $lcp_element ) {
+ // No LCP element at this breakpoint, so nothing to preload.
+ continue;
+ }
+
+ $media_query = sprintf( 'screen and ( min-width: %dpx )', $minimum_viewport_widths[ $i ] );
+ if ( isset( $minimum_viewport_widths[ $i + 1 ] ) ) {
+ $media_query .= sprintf( ' and ( max-width: %dpx )', $minimum_viewport_widths[ $i + 1 ] - 1 );
+ }
+
+ }
+
+ return '';
+}
+
+function ilo_find_element_by_breadcrumbs( string $html, array $breadcrumbs ): array {
+ $p = new WP_HTML_Tag_Processor( $html );
+
+ /*
+ * The keys for the following two arrays correspond to each other. Given the following document:
+ *
+ *
+ *
+ *
+ *
+ * Hello!
+ *
+ *
+ *
+ *
+ * The two upon processing the IMG element, the two arrays should be equal to the following:
+ *
+ * $open_stack_tags = array( 'HTML', 'BODY', 'IMG' );
+ * $open_stack_indices = array( 0, 1, 1 );
+ */
+ $open_stack_tags = array();
+ $open_stack_indices = array();
+ while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
+ $tag_name = $p->get_tag();
+ if ( ! $p->is_tag_closer() ) {
+
+ // Close an open P tag when a P-closing tag is encountered.
+ if ( in_array( $tag_name, ILO_P_CLOSING_TAGS, true ) ) {
+ $i = array_search( 'P', $open_stack_tags, true );
+ if ( false !== $i ) {
+ array_splice( $open_stack_tags, $i );
+ array_splice( $open_stack_indices, count( $open_stack_tags ) );
+ }
+ }
+
+ $level = count( $open_stack_tags );
+ $open_stack_tags[] = $tag_name;
+
+ if ( ! isset( $open_stack_indices[ $level ] ) ) {
+ $open_stack_indices[ $level ] = 0;
+ } elseif ( ! ( 'DIV' === $tag_name && $p->get_attribute( 'id' ) === 'wpadminbar' ) ) {
+ // Only increment the tag index at this level only if it isn't the admin bar, since the presence of the
+ // admin bar can throw off the indices.
+ ++$open_stack_indices[ $level ];
+ }
+
+ // TODO: Now check if $open_stack matches breadcrumbs.
+
+ // Immediately pop off self-closing tags.
+ if ( in_array( $tag_name, ILO_SELF_CLOSING_TAGS, true ) ) {
+ array_pop( $open_stack_tags );
+ }
+ } else {
+ // If the closing tag is for self-closing tag, we ignore it since it was already handled above.
+ if ( in_array( $tag_name, ILO_SELF_CLOSING_TAGS, true ) ) {
+ continue;
+ }
+
+ // Since SVG and MathML can have a lot more self-closing/empty tags, potentially pop off the stack until getting to the open tag.
+ $did_splice = false;
+ if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
+ $i = array_search( $tag_name, $open_stack_tags, true );
+ if ( false !== $i ) {
+ array_splice( $open_stack_tags, $i );
+ $did_splice = true;
+ }
+ }
+
+ if ( ! $did_splice ) {
+ $popped_tag_name = array_pop( $open_stack_tags );
+ if ( $popped_tag_name !== $tag_name ) {
+ error_log( "Expected popped tag stack element {$popped_tag_name} to match the currently visited closing tag $tag_name." ); // phpcs:ignore
+ }
+ }
+ array_splice( $open_stack_indices, count( $open_stack_tags ) + 1 );
+ }
+
+ // ...
+ $src = $p->get_attribute( 'src' );
+ $srcset = $p->get_attribute( 'srcset' );
+ }
+
+ return array();
+}
+
+function ilo_remove_fetchpriority_from_all_images( string $html ): string {
+ $p = new WP_HTML_Tag_Processor( $html );
+ while ( $p->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
+ if ( $p->get_attribute( 'fetchpriority' ) ) {
+ $p->set_attribute( 'data-wp-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
+ $p->remove_attribute( 'fetchpriority' );
+ }
+ }
+ return $p->get_updated_html();
+}
+
/**
* Optimizes template output buffer.
*
@@ -28,24 +207,36 @@ function ilo_maybe_add_template_output_buffer_filter() {
* @return string Filtered template output buffer.
*/
function ilo_optimize_template_output_buffer( string $buffer ): string {
- $slug = ilo_get_url_metrics_slug( ilo_get_normalized_query_vars() );
- $post = ilo_get_url_metrics_post( $slug );
- $page_metrics = ilo_parse_stored_url_metrics( $post );
+ $slug = ilo_get_url_metrics_slug( ilo_get_normalized_query_vars() );
+ $post = ilo_get_url_metrics_post( $slug );
+ $url_metrics = ilo_parse_stored_url_metrics( $post );
- $lcp_images_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $page_metrics, ilo_get_breakpoint_max_widths() );
+ $lcp_images_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
+ // TODO: We need to walk the document to find the breadcrumbs.
if ( ! empty( $lcp_images_by_minimum_viewport_widths ) ) {
- if ( count( $lcp_images_by_minimum_viewport_widths ) !== 1 ) {
- $p = new WP_HTML_Tag_Processor( $buffer );
- while ( $p->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
- if ( $p->get_attribute( 'fetchpriority' ) ) {
- $p->set_attribute( 'data-wp-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
- $p->remove_attribute( 'fetchpriority' );
- }
- }
- $buffer = $p->get_updated_html();
+ $breakpoint_count_with_lcp_images = count( array_filter( $lcp_images_by_minimum_viewport_widths ) );
+
+ if ( 1 === count( $lcp_images_by_minimum_viewport_widths ) && 1 === $breakpoint_count_with_lcp_images ) {
+ // If there is exactly one LCP image for all breakpoints, ensure fetchpriority is set on that image only.
+ $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+
+ $lcp_element = current( $lcp_images_by_minimum_viewport_widths );
+
+ } elseif ( 0 === $breakpoint_count_with_lcp_images ) {
+ // If there are no LCP images, remove fetchpriority from all images.
+ $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+ } else {
+ // Otherwise, there are two or more breakpoints have different LCP images, so we must remove fetchpriority
+ // from all images and add breakpoint-specific preload links.
+ $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+
+ // TODO: We need to locate the elements by their breadcrumbs.
+ ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
+
}
}
return $buffer;
}
+
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index aaa7c98500..a304eca6fd 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -136,7 +136,7 @@ function ilo_register_endpoint() {
'items' => array(
'type' => 'object',
'properties' => array(
- 'tagName' => array(
+ 'tagName' => array( // TODO: Should this just be 'tag' instead?
'type' => 'string',
'required' => true,
'pattern' => '^[a-zA-Z0-9-]+$',
From c20f967239265fe87fd6c4f59964764a2e605dca Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Fri, 17 Nov 2023 18:12:55 -0800
Subject: [PATCH 097/371] Set appropriate fetchpriority and otherwise add
preload links
---
.../detection/detect.js | 9 ++
.../optimization.php | 150 ++++++++++++++----
2 files changed, 124 insertions(+), 35 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 431ade5a24..fa33a0b753 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -116,6 +116,15 @@ function getElementIndex( element ) {
const children = [ ...element.parentElement.children ];
let index = children.indexOf( element );
if ( children.includes( document.getElementById( adminBarId ) ) ) {
+ // TODO: Should detection just be turned off when is_user_logged_in()?
+ --index;
+ }
+ if (
+ children.includes(
+ document.querySelector( '.skip-link.screen-reader-text' )
+ )
+ ) {
+ // TODO: This is not good.
--index;
}
return index;
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 8934daf833..c3f6b08ac8 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -85,26 +85,66 @@ function ilo_maybe_add_template_output_buffer_filter() {
}
add_action( 'wp', 'ilo_maybe_add_template_output_buffer_filter' );
+/**
+ * Constructs preload links.
+ *
+ * @param array $lcp_images_by_minimum_viewport_widths LCP images keyed by minimum viewport width, amended with attributes key for the IMG attributes.
+ * @return string Markup for one or more preload link tags.
+ */
function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widths ): string {
+ $preload_links = array();
+
$minimum_viewport_widths = array_keys( $lcp_images_by_minimum_viewport_widths );
for ( $i = 0, $len = count( $minimum_viewport_widths ); $i < $len; $i++ ) {
$lcp_element = $lcp_images_by_minimum_viewport_widths[ $minimum_viewport_widths[ $i ] ];
- if ( false === $lcp_element ) {
+ if ( false === $lcp_element || empty( $lcp_element['attributes'] ) ) {
// No LCP element at this breakpoint, so nothing to preload.
continue;
}
+ $img_attributes = $lcp_element['attributes'];
+
+ // Prevent preloading src for browsers that don't support imagesrcset on the link element.
+ if ( isset( $img_attributes['src'], $img_attributes['srcset'] ) ) {
+ unset( $img_attributes['src'] );
+ }
+
+ // Add media query.
$media_query = sprintf( 'screen and ( min-width: %dpx )', $minimum_viewport_widths[ $i ] );
if ( isset( $minimum_viewport_widths[ $i + 1 ] ) ) {
$media_query .= sprintf( ' and ( max-width: %dpx )', $minimum_viewport_widths[ $i + 1 ] - 1 );
}
+ $img_attributes['media'] = $media_query;
+
+ // Construct preload link.
+ $link_tag = ' $value ) {
+ // Map img attribute name to link attribute name.
+ if ( 'srcset' === $name || 'sizes' === $name ) {
+ $name = 'image' . $name;
+ } elseif ( 'src' === $name ) {
+ $name = 'href';
+ }
+
+ $link_tag .= sprintf( ' %s="%s"', $name, esc_attr( $value ) );
+ }
+ $link_tag .= '>';
+ $preload_links[] = $link_tag;
}
- return '';
+ return implode( "\n", $preload_links );
}
-function ilo_find_element_by_breadcrumbs( string $html, array $breadcrumbs ): array {
+/**
+ * Walks the provided HTML document, invoking the callback at each open tag.
+ *
+ * @param string $html Complete HTML document.
+ * @param callable $open_tag_callback Callback to invoke at each open tag. Callback is passed instance of
+ * WP_HTML_Tag_Processor as well as the breadcrumbs for the current element.
+ * @return string Updated HTML if modified by callback.
+ */
+function ilo_walk_document( string $html, callable $open_tag_callback ): string {
$p = new WP_HTML_Tag_Processor( $html );
/*
@@ -144,13 +184,28 @@ function ilo_find_element_by_breadcrumbs( string $html, array $breadcrumbs ): ar
if ( ! isset( $open_stack_indices[ $level ] ) ) {
$open_stack_indices[ $level ] = 0;
- } elseif ( ! ( 'DIV' === $tag_name && $p->get_attribute( 'id' ) === 'wpadminbar' ) ) {
- // Only increment the tag index at this level only if it isn't the admin bar, since the presence of the
- // admin bar can throw off the indices.
+ } else {
++$open_stack_indices[ $level ];
}
- // TODO: Now check if $open_stack matches breadcrumbs.
+ // TODO: We should consider not collecting metrics when the admin bar is shown and the user is logged-in.
+ // Only increment the tag index at this level only if it isn't the admin bar, since the presence of the
+ // admin bar can throw off the indices.
+ if ( 'DIV' === $tag_name && $p->get_attribute( 'id' ) === 'wpadminbar' ) {
+ --$open_stack_indices[ $level ];
+ }
+
+ // Construct the breadcrumbs to match the format from detect.js.
+ $breadcrumbs = array();
+ foreach ( $open_stack_tags as $i => $breadcrumb_tag_name ) {
+ $breadcrumbs[] = array(
+ 'tagName' => $breadcrumb_tag_name,
+ 'index' => $open_stack_indices[ $i ],
+ );
+ }
+
+ // Invoke the callback to do processing.
+ $open_tag_callback( $p, $breadcrumbs );
// Immediately pop off self-closing tags.
if ( in_array( $tag_name, ILO_SELF_CLOSING_TAGS, true ) ) {
@@ -180,24 +235,21 @@ function ilo_find_element_by_breadcrumbs( string $html, array $breadcrumbs ): ar
}
array_splice( $open_stack_indices, count( $open_stack_tags ) + 1 );
}
-
- // ...
- $src = $p->get_attribute( 'src' );
- $srcset = $p->get_attribute( 'srcset' );
}
- return array();
+ return $p->get_updated_html();
}
-function ilo_remove_fetchpriority_from_all_images( string $html ): string {
- $p = new WP_HTML_Tag_Processor( $html );
- while ( $p->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
- if ( $p->get_attribute( 'fetchpriority' ) ) {
- $p->set_attribute( 'data-wp-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
- $p->remove_attribute( 'fetchpriority' );
- }
+/**
+ * Removes fetchpriority from the current tag if present.
+ *
+ * @param WP_HTML_Tag_Processor $p Processor instance.
+ */
+function ilo_remove_fetchpriority_from_current_tag_processor_node( WP_HTML_Tag_Processor $p ) {
+ if ( $p->get_attribute( 'fetchpriority' ) ) {
+ $p->set_attribute( 'data-ilo-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
+ $p->remove_attribute( 'fetchpriority' );
}
- return $p->get_updated_html();
}
/**
@@ -213,30 +265,58 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
$lcp_images_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
- // TODO: We need to walk the document to find the breadcrumbs.
if ( ! empty( $lcp_images_by_minimum_viewport_widths ) ) {
- $breakpoint_count_with_lcp_images = count( array_filter( $lcp_images_by_minimum_viewport_widths ) );
-
- if ( 1 === count( $lcp_images_by_minimum_viewport_widths ) && 1 === $breakpoint_count_with_lcp_images ) {
- // If there is exactly one LCP image for all breakpoints, ensure fetchpriority is set on that image only.
- $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+ $breakpoint_lcp_images = array_filter( $lcp_images_by_minimum_viewport_widths );
+ // If there is exactly one LCP image for all breakpoints, ensure fetchpriority is set on that image only.
+ if ( 1 === count( $lcp_images_by_minimum_viewport_widths ) && 1 === count( $breakpoint_lcp_images ) ) {
$lcp_element = current( $lcp_images_by_minimum_viewport_widths );
- } elseif ( 0 === $breakpoint_count_with_lcp_images ) {
- // If there are no LCP images, remove fetchpriority from all images.
- $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+ $buffer = ilo_walk_document(
+ $buffer,
+ static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( $lcp_element ) {
+ if ( 'IMG' !== $p->get_tag() ) {
+ return;
+ }
+ if ( $breadcrumbs === $lcp_element['breadcrumbs'] ) {
+ $p->set_attribute( 'fetchpriority', 'high' );
+ $p->set_attribute( 'data-ilo-added-fetchpriority', true );
+ } else {
+ ilo_remove_fetchpriority_from_current_tag_processor_node( $p );
+ }
+ }
+ );
+ // TODO: We could also add the preload links here.
} else {
- // Otherwise, there are two or more breakpoints have different LCP images, so we must remove fetchpriority
- // from all images and add breakpoint-specific preload links.
- $buffer = ilo_remove_fetchpriority_from_all_images( $buffer );
+ // If there is not exactly one LCP element, we need to remove fetchpriority from all images while also
+ // capturing the attributes from the LCP element which we can then use for preload links.
+ $buffer = ilo_walk_document(
+ $buffer,
+ static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( &$lcp_images_by_minimum_viewport_widths ) {
+ if ( 'IMG' !== $p->get_tag() ) {
+ return;
+ }
+ ilo_remove_fetchpriority_from_current_tag_processor_node( $p );
- // TODO: We need to locate the elements by their breadcrumbs.
- ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
+ // Capture the attributes from the LCP element to use in preload links.
+ if ( count( $lcp_images_by_minimum_viewport_widths ) > 1 ) {
+ foreach ( $lcp_images_by_minimum_viewport_widths as &$lcp_element ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $breadcrumbs ) {
+ $lcp_element['attributes'] = array();
+ foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
+ $lcp_element['attributes'][ $attr_name ] = $p->get_attribute( $attr_name );
+ }
+ }
+ }
+ }
+ }
+ );
+
+ $preload_links = ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
+ $buffer = str_replace( '', $preload_links . '', $buffer );
}
}
return $buffer;
}
-
From 08635fa32ae2683baf423f13ace2c2570c3440be Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 27 Nov 2023 16:45:12 -0800
Subject: [PATCH 098/371] Use preg_replace() with limit 1 for injection or
preload links at end of head
---
modules/images/image-loading-optimization/optimization.php | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index c3f6b08ac8..a4c22e073b 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -128,12 +128,12 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
$link_tag .= sprintf( ' %s="%s"', $name, esc_attr( $value ) );
}
- $link_tag .= '>';
+ $link_tag .= ">\n";
$preload_links[] = $link_tag;
}
- return implode( "\n", $preload_links );
+ return implode( '', $preload_links );
}
/**
@@ -314,7 +314,8 @@ static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( &$lcp_ima
$preload_links = ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
- $buffer = str_replace( '', $preload_links . '', $buffer );
+ // TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
+ $buffer = preg_replace( '#(?=)#', $preload_links, $buffer, 1 );
}
}
From ec55c9a91358b7b4e4c1ba472a6966e7ff549f85 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 27 Nov 2023 16:46:03 -0800
Subject: [PATCH 099/371] Improve variable naming
---
.../image-loading-optimization/storage/data.php | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 8d529cc0f5..0b00ae225e 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -342,27 +342,27 @@ function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, ar
}
// Now we need to merge the breakpoints when there is an LCP element common between them.
- $last_lcp_element = null;
+ $prev_lcp_element = null;
return array_filter(
$lcp_element_by_viewport_minimum_width,
- static function ( $lcp_element ) use ( &$last_lcp_element ) {
+ static function ( $lcp_element ) use ( &$prev_lcp_element ) {
$include = (
// First element in list.
- null === $last_lcp_element
+ null === $prev_lcp_element
||
- ( is_array( $last_lcp_element ) && is_array( $lcp_element )
+ ( is_array( $prev_lcp_element ) && is_array( $lcp_element )
?
// This breakpoint and previous breakpoint had LCP element, and they were not the same element.
- $last_lcp_element['breadcrumbs'] !== $lcp_element['breadcrumbs']
+ $prev_lcp_element['breadcrumbs'] !== $lcp_element['breadcrumbs']
:
// This LCP element and the last LCP element were not the same. In this case, either variable may be
// false or an array, but both cannot be an array. If both are false, we don't want to include since
// it is the same. If one is an array and the other is false, then do want to include because this
// indicates a difference at this breakpoint.
- $last_lcp_element !== $lcp_element
+ $prev_lcp_element !== $lcp_element
)
);
- $last_lcp_element = $lcp_element;
+ $prev_lcp_element = $lcp_element;
return $include;
}
);
From 6e0f8093bb8b3eefd8fd0766a4902f6a5b1af90c Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 27 Nov 2023 16:51:15 -0800
Subject: [PATCH 100/371] Add todos for doing breadcrumbs exclusively on server
---
.../images/image-loading-optimization/detection/detect.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index fa33a0b753..4ce20d054e 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -106,6 +106,8 @@ function error( ...message ) {
/**
* Gets element index among siblings.
*
+ * @todo Eliminate this in favor of doing all breadcrumb generation exclusively on the server.
+ *
* @param {Element} element Element.
* @return {number} Index.
*/
@@ -124,7 +126,6 @@ function getElementIndex( element ) {
document.querySelector( '.skip-link.screen-reader-text' )
)
) {
- // TODO: This is not good.
--index;
}
return index;
@@ -133,6 +134,8 @@ function getElementIndex( element ) {
/**
* Gets breadcrumbs for a given element.
*
+ * @todo Eliminate this in favor of doing all breadcrumb generation exclusively on the server.
+ *
* @param {Element} leafElement
* @return {Breadcrumb[]} Breadcrumbs.
*/
@@ -271,6 +274,7 @@ export default async function detect( {
/** @type {Map} */
const breadcrumbedElementsMap = new Map(
[ ...breadcrumbedImages, ...breadcrumbedElementsWithBackgrounds ].map(
+ // TODO: Instead of generating breadcrumbs here, rely instead on server-generated breadcrumbs that are added to a data attribute by the server.
( element ) => [ element, getBreadcrumbs( element ) ]
)
);
From e4299837e69e2cc3afc23faed257d925b0c2b3fa Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 27 Nov 2023 16:53:40 -0800
Subject: [PATCH 101/371] Account for HEAD closing tag possibly being
upper-case
---
modules/images/image-loading-optimization/optimization.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index a4c22e073b..69fdcf9188 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -315,7 +315,7 @@ static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( &$lcp_ima
$preload_links = ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
// TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
- $buffer = preg_replace( '#(?=)#', $preload_links, $buffer, 1 );
+ $buffer = preg_replace( '#(?=)#i', $preload_links, $buffer, 1 );
}
}
From 8974ac8e734651510fd2d15a34bb38e5ee70bacf Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Mon, 27 Nov 2023 17:01:04 -0800
Subject: [PATCH 102/371] Disable on Customizer preview and non-GET responses
---
.../images/image-loading-optimization/storage/data.php | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 0b00ae225e..6de16d7828 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -44,7 +44,14 @@ function ilo_get_url_metric_freshness_ttl(): int {
* @return bool Whether response can be optimized.
*/
function ilo_can_optimize_response(): bool {
- $able = ! is_search();
+ $able = ! (
+ // Since the URL space is infinite.
+ is_search() ||
+ // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context.
+ is_customize_preview() ||
+ // The images detected in the response body of a POST request cannot, by definition, be cached.
+ 'GET' !== $_SERVER['REQUEST_METHOD']
+ );
/**
* Filters whether the current response can be optimized.
From acde9b4d9e6e4943f52a95b86dc234b16e69a5bc Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 12:50:00 -0800
Subject: [PATCH 103/371] Introduce ILO_HTML_Tag_Processor
---
.../class-ilo-html-tag-processor.php | 310 ++++++++++++++++++
.../optimization.php | 219 ++-----------
2 files changed, 333 insertions(+), 196 deletions(-)
create mode 100644 modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
new file mode 100644
index 0000000000..5e170291e0
--- /dev/null
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -0,0 +1,310 @@
+ element.
+ * For example 'foo
bar ' should parse the same as 'foo
bar '.
+ *
+ * @link https://www.w3.org/TR/html-markup/p.html
+ * @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L262-L293
+ */
+ const P_CLOSING_TAGS = array(
+ 'ADDRESS',
+ 'ARTICLE',
+ 'ASIDE',
+ 'BLOCKQUOTE',
+ 'DIR',
+ 'DL',
+ 'FIELDSET',
+ 'FOOTER',
+ 'FORM',
+ 'H1',
+ 'H2',
+ 'H3',
+ 'H4',
+ 'H5',
+ 'H6',
+ 'HEADER',
+ 'HR',
+ 'MENU',
+ 'NAV',
+ 'OL',
+ 'P',
+ 'PRE',
+ 'SECTION',
+ 'TABLE',
+ 'UL',
+ );
+
+ /**
+ * Open stack tags.
+ *
+ * @var string[]
+ */
+ private $open_stack_tags = array();
+
+ /**
+ * Open stag indices.
+ *
+ * @var int[]
+ */
+ private $open_stack_indices = array();
+
+ /**
+ * Processor.
+ *
+ * @var WP_HTML_Tag_Processor
+ */
+ private $processor;
+
+ /**
+ * Constructor.
+ *
+ * @param string $html HTML to process.
+ */
+ public function __construct( string $html ) {
+ $this->processor = new WP_HTML_Tag_Processor( $html );
+ }
+
+ /**
+ * Walk over the document.
+ *
+ * Whenever an open tag is encountered, invoke the supplied $open_tag_callback and pass the tag name and breadcrumbs.
+ *
+ * @param Closure $open_tag_callback Open tag callback. The processor instance is passed as the sole argument.
+ */
+ public function walk( Closure $open_tag_callback ) {
+ $p = $this->processor;
+
+ /*
+ * The keys for the following two arrays correspond to each other. Given the following document:
+ *
+ *
+ *
+ *
+ *
+ * Hello!
+ *
+ *
+ *
+ *
+ * The two upon processing the IMG element, the two arrays should be equal to the following:
+ *
+ * $open_stack_tags = array( 'HTML', 'BODY', 'IMG' );
+ * $open_stack_indices = array( 0, 1, 1 );
+ */
+ $this->open_stack_tags = array();
+ $this->open_stack_indices = array();
+ while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
+ $tag_name = $p->get_tag();
+ if ( ! $p->is_tag_closer() ) {
+
+ // Close an open P tag when a P-closing tag is encountered.
+ if ( in_array( $tag_name, self::P_CLOSING_TAGS, true ) ) {
+ $i = array_search( 'P', $this->open_stack_tags, true );
+ if ( false !== $i ) {
+ array_splice( $this->open_stack_tags, $i );
+ array_splice( $this->open_stack_indices, count( $this->open_stack_tags ) );
+ }
+ }
+
+ $level = count( $this->open_stack_tags );
+ $this->open_stack_tags[] = $tag_name;
+
+ if ( ! isset( $this->open_stack_indices[ $level ] ) ) {
+ $this->open_stack_indices[ $level ] = 0;
+ } else {
+ ++$this->open_stack_indices[ $level ];
+ }
+
+ // TODO: We should consider not collecting metrics when the admin bar is shown and the user is logged-in.
+ // Only increment the tag index at this level only if it isn't the admin bar, since the presence of the
+ // admin bar can throw off the indices.
+ if ( 'DIV' === $tag_name && $p->get_attribute( 'id' ) === 'wpadminbar' ) {
+ --$this->open_stack_indices[ $level ];
+ }
+
+ // Invoke the callback to do processing.
+ $open_tag_callback( $tag_name, $this->get_breadcrumbs() );
+
+ // Immediately pop off self-closing tags.
+ if ( in_array( $tag_name, self::SELF_CLOSING_TAGS, true ) ) {
+ array_pop( $this->open_stack_tags );
+ }
+ } else {
+ // If the closing tag is for self-closing tag, we ignore it since it was already handled above.
+ if ( in_array( $tag_name, self::SELF_CLOSING_TAGS, true ) ) {
+ continue;
+ }
+
+ // Since SVG and MathML can have a lot more self-closing/empty tags, potentially pop off the stack until getting to the open tag.
+ $did_splice = false;
+ if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
+ $i = array_search( $tag_name, $this->open_stack_tags, true );
+ if ( false !== $i ) {
+ array_splice( $this->open_stack_tags, $i );
+ $did_splice = true;
+ }
+ }
+
+ if ( ! $did_splice ) {
+ $popped_tag_name = array_pop( $this->open_stack_tags );
+ if ( $popped_tag_name !== $tag_name ) {
+ error_log( "Expected popped tag stack element $popped_tag_name to match the currently visited closing tag $tag_name." ); // phpcs:ignore
+ }
+ }
+ array_splice( $this->open_stack_indices, count( $this->open_stack_tags ) + 1 );
+ }
+ }
+ }
+
+ /**
+ * Returns the uppercase name of the matched tag.
+ *
+ * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
+ * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+ *
+ * @see WP_HTML_Tag_Processor::get_tag()
+ *
+ * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
+ */
+ public function get_tag() {
+ return $this->processor->get_tag();
+ }
+
+ /**
+ * Gets breadcrumbs for the current open tag.
+ *
+ * Breadcrumbs are constructed to match the format from detect.js.
+ *
+ * @return array Breadcrumbs.
+ */
+ public function get_breadcrumbs(): array {
+ $breadcrumbs = array();
+ foreach ( $this->open_stack_tags as $i => $breadcrumb_tag_name ) {
+ $breadcrumbs[] = array(
+ 'tagName' => $breadcrumb_tag_name, // TODO: Just 'tag'.
+ 'index' => $this->open_stack_indices[ $i ],
+ );
+ }
+ return $breadcrumbs;
+ }
+
+ /**
+ * Removes the fetchpriority attribute from the current node being walked over.
+ *
+ * Also sets an attribute to indicate that the attribute was removed.
+ *
+ * @return bool Whether an attribute was removed.
+ */
+ public function remove_fetchpriority_attribute(): bool {
+ $p = $this->processor;
+ if ( $p->get_attribute( 'fetchpriority' ) ) {
+ $p->set_attribute( 'data-ilo-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
+ return $p->remove_attribute( 'fetchpriority' );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
+ *
+ * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
+ * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+ *
+ * @see WP_HTML_Tag_Processor::get_attribute()
+ *
+ * @param string $name Name of attribute whose value is requested.
+ * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`.
+ */
+ public function get_attribute( string $name ) {
+ return $this->processor->get_attribute( $name );
+ }
+
+ /**
+ * Updates or creates a new attribute on the currently matched tag with the passed value.
+ *
+ * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
+ * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+ *
+ * @see WP_HTML_Tag_Processor::set_attribute()
+ *
+ * @param string $name The attribute name to target.
+ * @param string|bool $value The new attribute value.
+ * @return bool Whether an attribute value was set.
+ */
+ public function set_attribute( string $name, $value ): bool {
+ return $this->processor->set_attribute( $name, $value );
+ }
+
+ /**
+ * Removes an attribute from the currently-matched tag.
+ *
+ * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
+ * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+ *
+ * @see WP_HTML_Tag_Processor::remove_attribute()
+ *
+ * @param string $name The attribute name to remove.
+ * @return bool Whether an attribute was removed.
+ */
+ public function remove_attribute( string $name ): bool {
+ return $this->processor->remove_attribute( $name );
+ }
+
+ /**
+ * Returns the string representation of the HTML Tag Processor.
+ *
+ * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
+ * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+ *
+ * @see WP_HTML_Tag_Processor::get_updated_html()
+ *
+ * @return string The processed HTML.
+ */
+ public function get_updated_html(): string {
+ return $this->processor->get_updated_html();
+ }
+}
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 69fdcf9188..8abd71df1c 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -10,69 +10,7 @@
exit; // Exit if accessed directly.
}
-/**
- * HTML elements that are self-closing.
- *
- * @link https://www.w3.org/TR/html5/syntax.html#serializing-html-fragments
- * @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L206-L232
- *
- * @var string[]
- */
-const ILO_SELF_CLOSING_TAGS = array(
- 'AREA',
- 'BASE',
- 'BASEFONT',
- 'BGSOUND',
- 'BR',
- 'COL',
- 'EMBED',
- 'FRAME',
- 'HR',
- 'IMG',
- 'INPUT',
- 'KEYGEN',
- 'LINK',
- 'META',
- 'PARAM',
- 'SOURCE',
- 'TRACK',
- 'WBR',
-);
-
-/**
- * The set of HTML tags whose presence will implicitly close a element.
- * For example '
foo
bar ' should parse the same as 'foo
bar '.
- *
- * @link https://www.w3.org/TR/html-markup/p.html
- * @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L262-L293
- */
-const ILO_P_CLOSING_TAGS = array(
- 'ADDRESS',
- 'ARTICLE',
- 'ASIDE',
- 'BLOCKQUOTE',
- 'DIR',
- 'DL',
- 'FIELDSET',
- 'FOOTER',
- 'FORM',
- 'H1',
- 'H2',
- 'H3',
- 'H4',
- 'H5',
- 'H6',
- 'HEADER',
- 'HR',
- 'MENU',
- 'NAV',
- 'OL',
- 'P',
- 'PRE',
- 'SECTION',
- 'TABLE',
- 'UL',
-);
+require_once __DIR__ . '/class-ilo-html-tag-processor.php';
/**
* Adds template output buffer filter for optimization if eligible.
@@ -136,122 +74,6 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
return implode( '', $preload_links );
}
-/**
- * Walks the provided HTML document, invoking the callback at each open tag.
- *
- * @param string $html Complete HTML document.
- * @param callable $open_tag_callback Callback to invoke at each open tag. Callback is passed instance of
- * WP_HTML_Tag_Processor as well as the breadcrumbs for the current element.
- * @return string Updated HTML if modified by callback.
- */
-function ilo_walk_document( string $html, callable $open_tag_callback ): string {
- $p = new WP_HTML_Tag_Processor( $html );
-
- /*
- * The keys for the following two arrays correspond to each other. Given the following document:
- *
- *
- *
- *
- *
- * Hello!
- *
- *
- *
- *
- * The two upon processing the IMG element, the two arrays should be equal to the following:
- *
- * $open_stack_tags = array( 'HTML', 'BODY', 'IMG' );
- * $open_stack_indices = array( 0, 1, 1 );
- */
- $open_stack_tags = array();
- $open_stack_indices = array();
- while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
- $tag_name = $p->get_tag();
- if ( ! $p->is_tag_closer() ) {
-
- // Close an open P tag when a P-closing tag is encountered.
- if ( in_array( $tag_name, ILO_P_CLOSING_TAGS, true ) ) {
- $i = array_search( 'P', $open_stack_tags, true );
- if ( false !== $i ) {
- array_splice( $open_stack_tags, $i );
- array_splice( $open_stack_indices, count( $open_stack_tags ) );
- }
- }
-
- $level = count( $open_stack_tags );
- $open_stack_tags[] = $tag_name;
-
- if ( ! isset( $open_stack_indices[ $level ] ) ) {
- $open_stack_indices[ $level ] = 0;
- } else {
- ++$open_stack_indices[ $level ];
- }
-
- // TODO: We should consider not collecting metrics when the admin bar is shown and the user is logged-in.
- // Only increment the tag index at this level only if it isn't the admin bar, since the presence of the
- // admin bar can throw off the indices.
- if ( 'DIV' === $tag_name && $p->get_attribute( 'id' ) === 'wpadminbar' ) {
- --$open_stack_indices[ $level ];
- }
-
- // Construct the breadcrumbs to match the format from detect.js.
- $breadcrumbs = array();
- foreach ( $open_stack_tags as $i => $breadcrumb_tag_name ) {
- $breadcrumbs[] = array(
- 'tagName' => $breadcrumb_tag_name,
- 'index' => $open_stack_indices[ $i ],
- );
- }
-
- // Invoke the callback to do processing.
- $open_tag_callback( $p, $breadcrumbs );
-
- // Immediately pop off self-closing tags.
- if ( in_array( $tag_name, ILO_SELF_CLOSING_TAGS, true ) ) {
- array_pop( $open_stack_tags );
- }
- } else {
- // If the closing tag is for self-closing tag, we ignore it since it was already handled above.
- if ( in_array( $tag_name, ILO_SELF_CLOSING_TAGS, true ) ) {
- continue;
- }
-
- // Since SVG and MathML can have a lot more self-closing/empty tags, potentially pop off the stack until getting to the open tag.
- $did_splice = false;
- if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) {
- $i = array_search( $tag_name, $open_stack_tags, true );
- if ( false !== $i ) {
- array_splice( $open_stack_tags, $i );
- $did_splice = true;
- }
- }
-
- if ( ! $did_splice ) {
- $popped_tag_name = array_pop( $open_stack_tags );
- if ( $popped_tag_name !== $tag_name ) {
- error_log( "Expected popped tag stack element {$popped_tag_name} to match the currently visited closing tag $tag_name." ); // phpcs:ignore
- }
- }
- array_splice( $open_stack_indices, count( $open_stack_tags ) + 1 );
- }
- }
-
- return $p->get_updated_html();
-}
-
-/**
- * Removes fetchpriority from the current tag if present.
- *
- * @param WP_HTML_Tag_Processor $p Processor instance.
- */
-function ilo_remove_fetchpriority_from_current_tag_processor_node( WP_HTML_Tag_Processor $p ) {
- if ( $p->get_attribute( 'fetchpriority' ) ) {
- $p->set_attribute( 'data-ilo-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
- $p->remove_attribute( 'fetchpriority' );
- }
-}
-
/**
* Optimizes template output buffer.
*
@@ -272,45 +94,50 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
if ( 1 === count( $lcp_images_by_minimum_viewport_widths ) && 1 === count( $breakpoint_lcp_images ) ) {
$lcp_element = current( $lcp_images_by_minimum_viewport_widths );
- $buffer = ilo_walk_document(
- $buffer,
- static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( $lcp_element ) {
- if ( 'IMG' !== $p->get_tag() ) {
+ $processor = new ILO_HTML_Tag_Processor( $buffer );
+ $processor->walk(
+ static function () use ( $processor, $lcp_element ) {
+ if ( $processor->get_tag() !== 'IMG' ) {
return;
}
- if ( $breadcrumbs === $lcp_element['breadcrumbs'] ) {
- $p->set_attribute( 'fetchpriority', 'high' );
- $p->set_attribute( 'data-ilo-added-fetchpriority', true );
+
+ if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
} else {
- ilo_remove_fetchpriority_from_current_tag_processor_node( $p );
+ $processor->remove_fetchpriority_attribute();
}
}
);
+ $buffer = $processor->get_updated_html();
+
// TODO: We could also add the preload links here.
} else {
// If there is not exactly one LCP element, we need to remove fetchpriority from all images while also
// capturing the attributes from the LCP element which we can then use for preload links.
- $buffer = ilo_walk_document(
- $buffer,
- static function ( WP_HTML_Tag_Processor $p, array $breadcrumbs ) use ( &$lcp_images_by_minimum_viewport_widths ) {
- if ( 'IMG' !== $p->get_tag() ) {
+ $processor = new ILO_HTML_Tag_Processor( $buffer );
+ $processor->walk(
+ static function () use ( $processor, &$lcp_images_by_minimum_viewport_widths ) {
+ if ( $processor->get_tag() !== 'IMG' ) {
return;
}
- ilo_remove_fetchpriority_from_current_tag_processor_node( $p );
- // Capture the attributes from the LCP element to use in preload links.
- if ( count( $lcp_images_by_minimum_viewport_widths ) > 1 ) {
+ $processor->remove_fetchpriority_attribute();
+
+ // Capture the attributes from the LCP elements to use in preload links.
+ if ( count( $lcp_images_by_minimum_viewport_widths ) > 1 ) { // TODO: Why?
foreach ( $lcp_images_by_minimum_viewport_widths as &$lcp_element ) {
- if ( $lcp_element && $lcp_element['breadcrumbs'] === $breadcrumbs ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
$lcp_element['attributes'] = array();
foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
- $lcp_element['attributes'][ $attr_name ] = $p->get_attribute( $attr_name );
+ $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
}
}
}
}
}
);
+ $buffer = $processor->get_updated_html();
$preload_links = ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
From 024719d0fec1be69e769a9b2eb23fb074c081169 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 13:40:11 -0800
Subject: [PATCH 104/371] Allow callable not just Closure
---
.../class-ilo-html-tag-processor.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 5e170291e0..ed6b0e3c55 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -112,9 +112,9 @@ public function __construct( string $html ) {
*
* Whenever an open tag is encountered, invoke the supplied $open_tag_callback and pass the tag name and breadcrumbs.
*
- * @param Closure $open_tag_callback Open tag callback. The processor instance is passed as the sole argument.
+ * @param callable $open_tag_callback Open tag callback. The processor instance is passed as the sole argument.
*/
- public function walk( Closure $open_tag_callback ) {
+ public function walk( callable $open_tag_callback ) {
$p = $this->processor;
/*
From 38cb821ffc694856631de3e3c759e569d5361bc1 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:06:55 -0800
Subject: [PATCH 105/371] Prevent error when no ilo_url_metrics post
---
modules/images/image-loading-optimization/optimization.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 8abd71df1c..5d81a51b1b 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -83,7 +83,7 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
function ilo_optimize_template_output_buffer( string $buffer ): string {
$slug = ilo_get_url_metrics_slug( ilo_get_normalized_query_vars() );
$post = ilo_get_url_metrics_post( $slug );
- $url_metrics = ilo_parse_stored_url_metrics( $post );
+ $url_metrics = $post ? ilo_parse_stored_url_metrics( $post ) : array(); // TODO: If $post is null, short circuit?
$lcp_images_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
From 234e2fed0ca7841d78e21e8444d1867895d218be Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:08:15 -0800
Subject: [PATCH 106/371] Clarify logic in ilo_optimize_template_output_buffer
---
.../optimization.php | 32 +++++++++++--------
.../storage/data.php | 2 +-
2 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 5d81a51b1b..6bea0eb455 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -85,14 +85,19 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
$post = ilo_get_url_metrics_post( $slug );
$url_metrics = $post ? ilo_parse_stored_url_metrics( $post ) : array(); // TODO: If $post is null, short circuit?
- $lcp_images_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
+ $lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
- if ( ! empty( $lcp_images_by_minimum_viewport_widths ) ) {
- $breakpoint_lcp_images = array_filter( $lcp_images_by_minimum_viewport_widths );
+ if ( ! empty( $lcp_elements_by_minimum_viewport_widths ) ) {
+ // TODO: What if we just don't have enough data for the other breakpoints yet? That is if count(ilo_group_url_metrics_by_breakpoint) !== count($breakpoint_max_widths)+1.
// If there is exactly one LCP image for all breakpoints, ensure fetchpriority is set on that image only.
- if ( 1 === count( $lcp_images_by_minimum_viewport_widths ) && 1 === count( $breakpoint_lcp_images ) ) {
- $lcp_element = current( $lcp_images_by_minimum_viewport_widths );
+ if (
+ // All breakpoints share the same LCP element (or all have none at all).
+ 1 === count( $lcp_elements_by_minimum_viewport_widths ) &&
+ // The breakpoints don't share a common lack of an LCP element.
+ ! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
+ ) {
+ $lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
$processor = new ILO_HTML_Tag_Processor( $buffer );
$processor->walk(
@@ -102,6 +107,7 @@ static function () use ( $processor, $lcp_element ) {
}
if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
+ // TODO: If it already has the attribute, include an attribute to indicate server-side heuristics were successful.
$processor->set_attribute( 'fetchpriority', 'high' );
$processor->set_attribute( 'data-ilo-added-fetchpriority', true );
} else {
@@ -117,7 +123,7 @@ static function () use ( $processor, $lcp_element ) {
// capturing the attributes from the LCP element which we can then use for preload links.
$processor = new ILO_HTML_Tag_Processor( $buffer );
$processor->walk(
- static function () use ( $processor, &$lcp_images_by_minimum_viewport_widths ) {
+ static function () use ( $processor, &$lcp_elements_by_minimum_viewport_widths ) {
if ( $processor->get_tag() !== 'IMG' ) {
return;
}
@@ -125,13 +131,11 @@ static function () use ( $processor, &$lcp_images_by_minimum_viewport_widths ) {
$processor->remove_fetchpriority_attribute();
// Capture the attributes from the LCP elements to use in preload links.
- if ( count( $lcp_images_by_minimum_viewport_widths ) > 1 ) { // TODO: Why?
- foreach ( $lcp_images_by_minimum_viewport_widths as &$lcp_element ) {
- if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
- $lcp_element['attributes'] = array();
- foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
- $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
- }
+ foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
+ $lcp_element['attributes'] = array();
+ foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
+ $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
}
}
}
@@ -139,7 +143,7 @@ static function () use ( $processor, &$lcp_images_by_minimum_viewport_widths ) {
);
$buffer = $processor->get_updated_html();
- $preload_links = ilo_construct_preload_links( $lcp_images_by_minimum_viewport_widths );
+ $preload_links = ilo_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
// TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
$buffer = preg_replace( '#(?=)#i', $preload_links, $buffer, 1 );
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 6de16d7828..33139a2f1b 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -305,7 +305,7 @@ static function ( $breakpoint ) {
*
* @param array $url_metrics URL metrics.
* @param int[] $breakpoint_max_widths Breakpoint max widths.
- * @return array LCP elements keyed by its minimum viewport width.
+ * @return array LCP elements keyed by its minimum viewport width. If there is no LCP element at a breakpoint, then `false` is used.
*/
function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, array $breakpoint_max_widths ): array {
$grouped_url_metrics = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
From 1585a7868272a47358ba7ee3f186dd5dd74b6332 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:10:55 -0800
Subject: [PATCH 107/371] Set attribute when server-side heuristics were
correct
---
.../images/image-loading-optimization/optimization.php | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 6bea0eb455..dcd2becb0d 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -107,9 +107,12 @@ static function () use ( $processor, $lcp_element ) {
}
if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
- // TODO: If it already has the attribute, include an attribute to indicate server-side heuristics were successful.
- $processor->set_attribute( 'fetchpriority', 'high' );
- $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
+ if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
+ $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
+ } else {
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
+ }
} else {
$processor->remove_fetchpriority_attribute();
}
From 292d1c4eecbf0956332996123be487c5053eebcc Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:27:21 -0800
Subject: [PATCH 108/371] Prevent setting fetchpriority on IMG when not all
breakpoints have data yet collected
---
.../optimization.php | 26 ++++++++++++++-----
.../storage/data.php | 6 ++---
2 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index dcd2becb0d..0a02a7bd6d 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -81,21 +81,33 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
* @return string Filtered template output buffer.
*/
function ilo_optimize_template_output_buffer( string $buffer ): string {
- $slug = ilo_get_url_metrics_slug( ilo_get_normalized_query_vars() );
- $post = ilo_get_url_metrics_post( $slug );
- $url_metrics = $post ? ilo_parse_stored_url_metrics( $post ) : array(); // TODO: If $post is null, short circuit?
+ $slug = ilo_get_url_metrics_slug( ilo_get_normalized_query_vars() );
+ $post = ilo_get_url_metrics_post( $slug );
- $lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics, ilo_get_breakpoint_max_widths() );
+ // No URL metrics are present, so there's nothing we can do.
+ if ( ! $post ) {
+ return $buffer;
+ }
+
+ $url_metrics = ilo_parse_stored_url_metrics( $post );
+
+ $breakpoint_max_widths = ilo_get_breakpoint_max_widths();
+ $url_metrics_grouped_by_breakpoint = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
+ $lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics_grouped_by_breakpoint );
if ( ! empty( $lcp_elements_by_minimum_viewport_widths ) ) {
- // TODO: What if we just don't have enough data for the other breakpoints yet? That is if count(ilo_group_url_metrics_by_breakpoint) !== count($breakpoint_max_widths)+1.
- // If there is exactly one LCP image for all breakpoints, ensure fetchpriority is set on that image only.
+ // TODO: Handle case when the LCP element is not an image at all, but rather a background-image.
+ // Use the fetchpriority attribute on the image when all breakpoints have the same LCP element.
if (
// All breakpoints share the same LCP element (or all have none at all).
- 1 === count( $lcp_elements_by_minimum_viewport_widths ) &&
+ 1 === count( $lcp_elements_by_minimum_viewport_widths )
+ &&
// The breakpoints don't share a common lack of an LCP element.
! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
+ &&
+ // All breakpoints have URL metrics being reported.
+ count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1
) {
$lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 33139a2f1b..331033ca77 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -303,12 +303,10 @@ static function ( $breakpoint ) {
* breakpoint, then the array value is an array representing that element, including its breadcrumbs. If two adjoining
* breakpoints have the same value, then the latter is dropped.
*
- * @param array $url_metrics URL metrics.
- * @param int[] $breakpoint_max_widths Breakpoint max widths.
+ * @param array $grouped_url_metrics URL metrics grouped by breakpoint. See `ilo_group_url_metrics_by_breakpoint()`.
* @return array LCP elements keyed by its minimum viewport width. If there is no LCP element at a breakpoint, then `false` is used.
*/
-function ilo_get_lcp_elements_by_minimum_viewport_widths( array $url_metrics, array $breakpoint_max_widths ): array {
- $grouped_url_metrics = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
+function ilo_get_lcp_elements_by_minimum_viewport_widths( array $grouped_url_metrics ): array {
$lcp_element_by_viewport_minimum_width = array();
foreach ( $grouped_url_metrics as $viewport_minimum_width => $breakpoint_url_metrics ) {
From fca7f8d07b20d92a56f4d6e40156997b0706da16 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:28:36 -0800
Subject: [PATCH 109/371] Remove needless if statement
---
.../optimization.php | 111 +++++++++---------
1 file changed, 54 insertions(+), 57 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 0a02a7bd6d..7fd6597be7 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -95,74 +95,71 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
$url_metrics_grouped_by_breakpoint = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
$lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics_grouped_by_breakpoint );
- if ( ! empty( $lcp_elements_by_minimum_viewport_widths ) ) {
-
- // TODO: Handle case when the LCP element is not an image at all, but rather a background-image.
- // Use the fetchpriority attribute on the image when all breakpoints have the same LCP element.
- if (
- // All breakpoints share the same LCP element (or all have none at all).
- 1 === count( $lcp_elements_by_minimum_viewport_widths )
- &&
- // The breakpoints don't share a common lack of an LCP element.
- ! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
- &&
- // All breakpoints have URL metrics being reported.
- count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1
- ) {
- $lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
-
- $processor = new ILO_HTML_Tag_Processor( $buffer );
- $processor->walk(
- static function () use ( $processor, $lcp_element ) {
- if ( $processor->get_tag() !== 'IMG' ) {
- return;
- }
+ // TODO: Handle case when the LCP element is not an image at all, but rather a background-image.
+ // Use the fetchpriority attribute on the image when all breakpoints have the same LCP element.
+ if (
+ // All breakpoints share the same LCP element (or all have none at all).
+ 1 === count( $lcp_elements_by_minimum_viewport_widths )
+ &&
+ // The breakpoints don't share a common lack of an LCP element.
+ ! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
+ &&
+ // All breakpoints have URL metrics being reported.
+ count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1
+ ) {
+ $lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
+
+ $processor = new ILO_HTML_Tag_Processor( $buffer );
+ $processor->walk(
+ static function () use ( $processor, $lcp_element ) {
+ if ( $processor->get_tag() !== 'IMG' ) {
+ return;
+ }
- if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
- if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
- $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
- } else {
- $processor->set_attribute( 'fetchpriority', 'high' );
- $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
- }
+ if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
+ if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
+ $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
} else {
- $processor->remove_fetchpriority_attribute();
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
}
+ } else {
+ $processor->remove_fetchpriority_attribute();
+ }
+ }
+ );
+ $buffer = $processor->get_updated_html();
+
+ // TODO: We could also add the preload links here.
+ } else {
+ // If there is not exactly one LCP element, we need to remove fetchpriority from all images while also
+ // capturing the attributes from the LCP element which we can then use for preload links.
+ $processor = new ILO_HTML_Tag_Processor( $buffer );
+ $processor->walk(
+ static function () use ( $processor, &$lcp_elements_by_minimum_viewport_widths ) {
+ if ( $processor->get_tag() !== 'IMG' ) {
+ return;
}
- );
- $buffer = $processor->get_updated_html();
-
- // TODO: We could also add the preload links here.
- } else {
- // If there is not exactly one LCP element, we need to remove fetchpriority from all images while also
- // capturing the attributes from the LCP element which we can then use for preload links.
- $processor = new ILO_HTML_Tag_Processor( $buffer );
- $processor->walk(
- static function () use ( $processor, &$lcp_elements_by_minimum_viewport_widths ) {
- if ( $processor->get_tag() !== 'IMG' ) {
- return;
- }
- $processor->remove_fetchpriority_attribute();
+ $processor->remove_fetchpriority_attribute();
- // Capture the attributes from the LCP elements to use in preload links.
- foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
- if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
- $lcp_element['attributes'] = array();
- foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
- $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
- }
+ // Capture the attributes from the LCP elements to use in preload links.
+ foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
+ $lcp_element['attributes'] = array();
+ foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
+ $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
}
}
}
- );
- $buffer = $processor->get_updated_html();
+ }
+ );
+ $buffer = $processor->get_updated_html();
- $preload_links = ilo_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
+ $preload_links = ilo_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
- // TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
- $buffer = preg_replace( '#(?=)#i', $preload_links, $buffer, 1 );
- }
+ // TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
+ $buffer = preg_replace( '#(?=)#i', $preload_links, $buffer, 1 );
}
return $buffer;
From 3cda8873bf4bfe84e72e0724ae512a35027f6b6b Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:34:47 -0800
Subject: [PATCH 110/371] Disable background image detection until implemented
on server
---
.../detection/detect.js | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 4ce20d054e..45a8d25f0c 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -265,15 +265,18 @@ export default async function detect( {
const breadcrumbedImages = doc.body.querySelectorAll( 'img' );
// We do the same for elements with background images which are not data: URLs.
- const breadcrumbedElementsWithBackgrounds = Array.from(
- doc.body.querySelectorAll( '[style*="background"]' )
- ).filter( ( /** @type {Element} */ el ) =>
- /url\(\s*['"](?!=data:)/.test( el.style.backgroundImage )
- );
+ // TODO: Re-enable background image support when server-side is implemented.
+ // const breadcrumbedElementsWithBackgrounds = Array.from(
+ // doc.body.querySelectorAll( '[style*="background"]' )
+ // ).filter( ( /** @type {Element} */ el ) =>
+ // /url\(\s*['"](?!=data:)/.test( el.style.backgroundImage )
+ // );
/** @type {Map} */
const breadcrumbedElementsMap = new Map(
- [ ...breadcrumbedImages, ...breadcrumbedElementsWithBackgrounds ].map(
+ [
+ ...breadcrumbedImages /*, ...breadcrumbedElementsWithBackgrounds*/,
+ ].map(
// TODO: Instead of generating breadcrumbs here, rely instead on server-generated breadcrumbs that are added to a data attribute by the server.
( element ) => [ element, getBreadcrumbs( element ) ]
)
From f00addc80f6c222578ef30e9a12170046e050ea3 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:39:22 -0800
Subject: [PATCH 111/371] Use tag instead of tagName in breadcrumbs
---
.../class-ilo-html-tag-processor.php | 6 +++---
.../images/image-loading-optimization/detection/detect.js | 6 +++---
.../images/image-loading-optimization/storage/rest-api.php | 4 ++--
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index ed6b0e3c55..e412b43e79 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -218,14 +218,14 @@ public function get_tag() {
*
* Breadcrumbs are constructed to match the format from detect.js.
*
- * @return array Breadcrumbs.
+ * @return array Breadcrumbs.
*/
public function get_breadcrumbs(): array {
$breadcrumbs = array();
foreach ( $this->open_stack_tags as $i => $breadcrumb_tag_name ) {
$breadcrumbs[] = array(
- 'tagName' => $breadcrumb_tag_name, // TODO: Just 'tag'.
- 'index' => $this->open_stack_indices[ $i ],
+ 'tag' => $breadcrumb_tag_name,
+ 'index' => $this->open_stack_indices[ $i ],
);
}
return $breadcrumbs;
diff --git a/modules/images/image-loading-optimization/detection/detect.js b/modules/images/image-loading-optimization/detection/detect.js
index 45a8d25f0c..9619f39e8d 100644
--- a/modules/images/image-loading-optimization/detection/detect.js
+++ b/modules/images/image-loading-optimization/detection/detect.js
@@ -80,8 +80,8 @@ function error( ...message ) {
/**
* @typedef {Object} Breadcrumb
- * @property {number} index - Index of element among sibling elements.
- * @property {string} tagName - Tag name.
+ * @property {number} index - Index of element among sibling elements.
+ * @property {string} tag - Tag name.
*/
/**
@@ -146,7 +146,7 @@ function getBreadcrumbs( leafElement ) {
let element = leafElement;
while ( element instanceof Element ) {
breadcrumbs.unshift( {
- tagName: element.tagName,
+ tag: element.tagName,
index: getElementIndex( element ),
} );
element = element.parentElement;
diff --git a/modules/images/image-loading-optimization/storage/rest-api.php b/modules/images/image-loading-optimization/storage/rest-api.php
index a304eca6fd..617cde3fa8 100644
--- a/modules/images/image-loading-optimization/storage/rest-api.php
+++ b/modules/images/image-loading-optimization/storage/rest-api.php
@@ -136,12 +136,12 @@ function ilo_register_endpoint() {
'items' => array(
'type' => 'object',
'properties' => array(
- 'tagName' => array( // TODO: Should this just be 'tag' instead?
+ 'tag' => array(
'type' => 'string',
'required' => true,
'pattern' => '^[a-zA-Z0-9-]+$',
),
- 'index' => array(
+ 'index' => array(
'type' => 'int',
'required' => true,
'minimum' => 0,
From ce1db2fa4ff4c66b42525af6daf6fdfc1434718b Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 15:54:51 -0800
Subject: [PATCH 112/371] Prevent adding media query to preload link when just
min-width:0
---
.../image-loading-optimization/optimization.php | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 7fd6597be7..07f599215b 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -47,12 +47,14 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
unset( $img_attributes['src'] );
}
- // Add media query.
- $media_query = sprintf( 'screen and ( min-width: %dpx )', $minimum_viewport_widths[ $i ] );
- if ( isset( $minimum_viewport_widths[ $i + 1 ] ) ) {
- $media_query .= sprintf( ' and ( max-width: %dpx )', $minimum_viewport_widths[ $i + 1 ] - 1 );
+ // Add media query if it's going to be something other than just `min-width: 0px`.
+ if ( $minimum_viewport_widths[ $i ] > 0 || isset( $minimum_viewport_widths[ $i + 1 ] ) ) {
+ $media_query = sprintf( '( min-width: %dpx )', $minimum_viewport_widths[ $i ] );
+ if ( isset( $minimum_viewport_widths[ $i + 1 ] ) ) {
+ $media_query .= sprintf( ' and ( max-width: %dpx )', $minimum_viewport_widths[ $i + 1 ] - 1 );
+ }
+ $img_attributes['media'] = $media_query;
}
- $img_attributes['media'] = $media_query;
// Construct preload link.
$link_tag = '
Date: Tue, 28 Nov 2023 16:00:28 -0800
Subject: [PATCH 113/371] Add preload links always and consolidate code paths
---
.../optimization.php | 83 +++++++++----------
1 file changed, 39 insertions(+), 44 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 07f599215b..5c8c99238a 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -98,7 +98,7 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
$lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics_grouped_by_breakpoint );
// TODO: Handle case when the LCP element is not an image at all, but rather a background-image.
- // Use the fetchpriority attribute on the image when all breakpoints have the same LCP element.
+ // Prepare to set fetchpriority attribute on the image when all breakpoints have the same LCP element.
if (
// All breakpoints share the same LCP element (or all have none at all).
1 === count( $lcp_elements_by_minimum_viewport_widths )
@@ -109,59 +109,54 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
// All breakpoints have URL metrics being reported.
count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1
) {
- $lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
-
- $processor = new ILO_HTML_Tag_Processor( $buffer );
- $processor->walk(
- static function () use ( $processor, $lcp_element ) {
- if ( $processor->get_tag() !== 'IMG' ) {
- return;
- }
+ $common_lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
+ } else {
+ $common_lcp_element = null;
+ }
- if ( $processor->get_breadcrumbs() === $lcp_element['breadcrumbs'] ) {
- if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
- $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
- } else {
- $processor->set_attribute( 'fetchpriority', 'high' );
- $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
- }
- } else {
- $processor->remove_fetchpriority_attribute();
- }
+ // Walk over all IMG tags in the document and ensure fetchpriority is set/removed, and gather IMG attributes for preloading.
+ $processor = new ILO_HTML_Tag_Processor( $buffer );
+ $processor->walk(
+ static function () use ( $processor, $common_lcp_element, &$lcp_elements_by_minimum_viewport_widths ) {
+ if ( $processor->get_tag() !== 'IMG' ) {
+ return;
}
- );
- $buffer = $processor->get_updated_html();
- // TODO: We could also add the preload links here.
- } else {
- // If there is not exactly one LCP element, we need to remove fetchpriority from all images while also
- // capturing the attributes from the LCP element which we can then use for preload links.
- $processor = new ILO_HTML_Tag_Processor( $buffer );
- $processor->walk(
- static function () use ( $processor, &$lcp_elements_by_minimum_viewport_widths ) {
- if ( $processor->get_tag() !== 'IMG' ) {
- return;
+ // Ensure the fetchpriority attribute is set on the element properly.
+ if ( $common_lcp_element && $processor->get_breadcrumbs() === $common_lcp_element['breadcrumbs'] ) {
+ if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
+ $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
+ } else {
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
}
-
+ } else {
$processor->remove_fetchpriority_attribute();
+ }
- // Capture the attributes from the LCP elements to use in preload links.
- foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
- if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
- $lcp_element['attributes'] = array();
- foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
- $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
- }
+ // Capture the attributes from the LCP elements to use in preload links.
+ foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
+ $lcp_element['attributes'] = array();
+ foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
+ $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
}
}
}
+ }
+ );
+ $buffer = $processor->get_updated_html();
+
+ // Inject any preload links at the end of the HEAD. In the future, WP_HTML_Processor could be used to do this injection.
+ // However, given the simple replacement here this is not essential.
+ $preload_links = ilo_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
+ if ( $preload_links ) {
+ $buffer = preg_replace(
+ '#(?=)#i',
+ $preload_links,
+ $buffer,
+ 1
);
- $buffer = $processor->get_updated_html();
-
- $preload_links = ilo_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
-
- // TODO: In the future, WP_HTML_Processor could be used to do this injection. However, given the simple replacement here this is not essential.
- $buffer = preg_replace( '#(?=)#i', $preload_links, $buffer, 1 );
}
return $buffer;
From f0ee6b2770472c2a8144e472896ff838438385d2 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:04:17 -0800
Subject: [PATCH 114/371] Add since and private access tags
---
.../class-ilo-html-tag-processor.php | 1 +
.../images/image-loading-optimization/optimization.php | 9 +++++++++
2 files changed, 10 insertions(+)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index e412b43e79..38d14196e2 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -10,6 +10,7 @@
* Subclass of WP_HTML_Tag_Processor that adds support for breadcrumbs and a visiting callback.
*
* @since n.e.x.t
+ * @access private
*/
class ILO_HTML_Tag_Processor {
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 5c8c99238a..fbde9b1960 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -14,6 +14,9 @@
/**
* Adds template output buffer filter for optimization if eligible.
+ *
+ * @since n.e.x.t
+ * @access private
*/
function ilo_maybe_add_template_output_buffer_filter() {
if ( ! ilo_can_optimize_response() ) {
@@ -26,6 +29,9 @@ function ilo_maybe_add_template_output_buffer_filter() {
/**
* Constructs preload links.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $lcp_images_by_minimum_viewport_widths LCP images keyed by minimum viewport width, amended with attributes key for the IMG attributes.
* @return string Markup for one or more preload link tags.
*/
@@ -79,6 +85,9 @@ function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widt
/**
* Optimizes template output buffer.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param string $buffer Template output buffer.
* @return string Filtered template output buffer.
*/
From 1e495dbdb9bf5b030f1f7152ff4b9b062c8d5c6c Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:30:29 -0800
Subject: [PATCH 115/371] Update return tag phpdoc for
ilo_construct_preload_links()
---
modules/images/image-loading-optimization/optimization.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index fbde9b1960..ad62e4064f 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -33,7 +33,7 @@ function ilo_maybe_add_template_output_buffer_filter() {
* @access private
*
* @param array $lcp_images_by_minimum_viewport_widths LCP images keyed by minimum viewport width, amended with attributes key for the IMG attributes.
- * @return string Markup for one or more preload link tags.
+ * @return string Markup for zero or more preload link tags.
*/
function ilo_construct_preload_links( array $lcp_images_by_minimum_viewport_widths ): string {
$preload_links = array();
From 8065801a75da549c25d79b663894a10410b4a029 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:31:01 -0800
Subject: [PATCH 116/371] Add missing since and private access tags to
ilo_get_lcp_elements_by_minimum_viewport_widths()
---
modules/images/image-loading-optimization/storage/data.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 331033ca77..3c5f20910b 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -303,6 +303,9 @@ static function ( $breakpoint ) {
* breakpoint, then the array value is an array representing that element, including its breadcrumbs. If two adjoining
* breakpoints have the same value, then the latter is dropped.
*
+ * @since n.e.x.t
+ * @access private
+ *
* @param array $grouped_url_metrics URL metrics grouped by breakpoint. See `ilo_group_url_metrics_by_breakpoint()`.
* @return array LCP elements keyed by its minimum viewport width. If there is no LCP element at a breakpoint, then `false` is used.
*/
From 131a9a0bcaab48c830c2e59bab5d40f609ed8d4f Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:33:20 -0800
Subject: [PATCH 117/371] Move GH comment into code comment
---
.../image-loading-optimization/class-ilo-html-tag-processor.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 38d14196e2..3646ced4a6 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -142,6 +142,8 @@ public function walk( callable $open_tag_callback ) {
if ( ! $p->is_tag_closer() ) {
// Close an open P tag when a P-closing tag is encountered.
+ // TODO: There are quite a few more cases of optional closing tags: https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
+ // Nevertheless, given WordPress's legacy of XHTML compatibility, the lack of closing tags may not be common enough to warrant worrying about any of them.
if ( in_array( $tag_name, self::P_CLOSING_TAGS, true ) ) {
$i = array_search( 'P', $this->open_stack_tags, true );
if ( false !== $i ) {
From ee83ba00365969e9bed18c65cab0f9cce114925f Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:46:46 -0800
Subject: [PATCH 118/371] Remove needless remove_fetchpriority_attribute method
---
.../class-ilo-html-tag-processor.php | 17 -----------------
.../image-loading-optimization/optimization.php | 5 +++--
.../image-loading-optimization/storage/data.php | 2 +-
3 files changed, 4 insertions(+), 20 deletions(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 3646ced4a6..ebf19d6aba 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -234,23 +234,6 @@ public function get_breadcrumbs(): array {
return $breadcrumbs;
}
- /**
- * Removes the fetchpriority attribute from the current node being walked over.
- *
- * Also sets an attribute to indicate that the attribute was removed.
- *
- * @return bool Whether an attribute was removed.
- */
- public function remove_fetchpriority_attribute(): bool {
- $p = $this->processor;
- if ( $p->get_attribute( 'fetchpriority' ) ) {
- $p->set_attribute( 'data-ilo-removed-fetchpriority', $p->get_attribute( 'fetchpriority' ) );
- return $p->remove_attribute( 'fetchpriority' );
- } else {
- return false;
- }
- }
-
/**
* Returns the value of a requested attribute from a matched tag opener if that attribute exists.
*
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index ad62e4064f..7aa7b10809 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -139,8 +139,9 @@ static function () use ( $processor, $common_lcp_element, &$lcp_elements_by_mini
$processor->set_attribute( 'fetchpriority', 'high' );
$processor->set_attribute( 'data-ilo-added-fetchpriority', true );
}
- } else {
- $processor->remove_fetchpriority_attribute();
+ } elseif ( $processor->get_attribute( 'fetchpriority' ) ) {
+ $processor->set_attribute( 'data-ilo-removed-fetchpriority', $processor->get_attribute( 'fetchpriority' ) );
+ $processor->remove_attribute( 'fetchpriority' );
}
// Capture the attributes from the LCP elements to use in preload links.
diff --git a/modules/images/image-loading-optimization/storage/data.php b/modules/images/image-loading-optimization/storage/data.php
index 3c5f20910b..506a88b6cb 100644
--- a/modules/images/image-loading-optimization/storage/data.php
+++ b/modules/images/image-loading-optimization/storage/data.php
@@ -349,7 +349,7 @@ function ilo_get_lcp_elements_by_minimum_viewport_widths( array $grouped_url_met
}
}
- // Now we need to merge the breakpoints when there is an LCP element common between them.
+ // Now merge the breakpoints when there is an LCP element common between them.
$prev_lcp_element = null;
return array_filter(
$lcp_element_by_viewport_minimum_width,
From 3d991286d80421a87a1922ef62f43594117a79ae Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 16:59:49 -0800
Subject: [PATCH 119/371] Prevent removing fetchpriority when all breakpoints
do not have URL metrics collected
---
.../images/image-loading-optimization/optimization.php | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 7aa7b10809..1e40e1ca87 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -105,6 +105,7 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
$breakpoint_max_widths = ilo_get_breakpoint_max_widths();
$url_metrics_grouped_by_breakpoint = ilo_group_url_metrics_by_breakpoint( $url_metrics, $breakpoint_max_widths );
$lcp_elements_by_minimum_viewport_widths = ilo_get_lcp_elements_by_minimum_viewport_widths( $url_metrics_grouped_by_breakpoint );
+ $all_breakpoints_have_url_metrics = count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1;
// TODO: Handle case when the LCP element is not an image at all, but rather a background-image.
// Prepare to set fetchpriority attribute on the image when all breakpoints have the same LCP element.
@@ -116,7 +117,7 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
&&
// All breakpoints have URL metrics being reported.
- count( array_filter( $url_metrics_grouped_by_breakpoint ) ) === count( $breakpoint_max_widths ) + 1
+ $all_breakpoints_have_url_metrics
) {
$common_lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
} else {
@@ -126,7 +127,7 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
// Walk over all IMG tags in the document and ensure fetchpriority is set/removed, and gather IMG attributes for preloading.
$processor = new ILO_HTML_Tag_Processor( $buffer );
$processor->walk(
- static function () use ( $processor, $common_lcp_element, &$lcp_elements_by_minimum_viewport_widths ) {
+ static function () use ( $processor, $common_lcp_element, $all_breakpoints_have_url_metrics, &$lcp_elements_by_minimum_viewport_widths ) {
if ( $processor->get_tag() !== 'IMG' ) {
return;
}
@@ -139,7 +140,9 @@ static function () use ( $processor, $common_lcp_element, &$lcp_elements_by_mini
$processor->set_attribute( 'fetchpriority', 'high' );
$processor->set_attribute( 'data-ilo-added-fetchpriority', true );
}
- } elseif ( $processor->get_attribute( 'fetchpriority' ) ) {
+ } elseif ( $all_breakpoints_have_url_metrics && $processor->get_attribute( 'fetchpriority' ) ) {
+ // Note: The $all_breakpoints_have_url_metrics condition here allows for server-side heuristics to
+ // continue to apply while waiting for all breakpoints to have metrics collected for them.
$processor->set_attribute( 'data-ilo-removed-fetchpriority', $processor->get_attribute( 'fetchpriority' ) );
$processor->remove_attribute( 'fetchpriority' );
}
From d42601cf3570e5d02f5dec0daf49b88578eb5159 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 17:15:45 -0800
Subject: [PATCH 120/371] Use wp_trigger_error() in ILO_HTML_Tag_Processor and
improve phpdoc
---
.../class-ilo-html-tag-processor.php | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index ebf19d6aba..0c72d866ec 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -7,12 +7,12 @@
*/
/**
- * Subclass of WP_HTML_Tag_Processor that adds support for breadcrumbs and a visiting callback.
+ * Processor leveraging WP_HTML_Tag_Processor which walks over a document and gathers breadcrumbs and invokes a callback for each open tag.
*
* @since n.e.x.t
* @access private
*/
-class ILO_HTML_Tag_Processor {
+final class ILO_HTML_Tag_Processor {
/**
* HTML elements that are self-closing.
@@ -193,8 +193,18 @@ public function walk( callable $open_tag_callback ) {
if ( ! $did_splice ) {
$popped_tag_name = array_pop( $this->open_stack_tags );
- if ( $popped_tag_name !== $tag_name ) {
- error_log( "Expected popped tag stack element $popped_tag_name to match the currently visited closing tag $tag_name." ); // phpcs:ignore
+ if ( $popped_tag_name !== $tag_name && function_exists( 'wp_trigger_error' ) ) {
+ wp_trigger_error(
+ __METHOD__,
+ esc_html(
+ sprintf(
+ /* translators: 1: Popped tag name, 2: Closing tag name */
+ __( 'Expected popped tag stack element %1$s to match the currently visited closing tag %2$s.', 'performance-lab' ),
+ $popped_tag_name,
+ $tag_name
+ )
+ )
+ );
}
}
array_splice( $this->open_stack_indices, count( $this->open_stack_tags ) + 1 );
From 4db5161b3f756a4e23e8c7eb1b02ce4db37bad65 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Tue, 28 Nov 2023 17:29:11 -0800
Subject: [PATCH 121/371] Add var phpdoc tag to class constant
---
.../image-loading-optimization/class-ilo-html-tag-processor.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 0c72d866ec..33861d2a1f 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -49,6 +49,8 @@ final class ILO_HTML_Tag_Processor {
*
* @link https://www.w3.org/TR/html-markup/p.html
* @link https://github.com/ampproject/amp-toolbox-php/blob/c79a0fe558a3c042aee4789bbf33376cca7a733d/src/Html/Tag.php#L262-L293
+ *
+ * @var string[]
*/
const P_CLOSING_TAGS = array(
'ADDRESS',
From 2c3cf3a945b661270155032d832711f7aee4701b Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 29 Nov 2023 13:03:56 -0800
Subject: [PATCH 122/371] Use a generator instead of a callback
---
.../class-ilo-html-tag-processor.php | 30 ++++-------
.../optimization.php | 50 +++++++++----------
2 files changed, 33 insertions(+), 47 deletions(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 33861d2a1f..9a4d373624 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -7,7 +7,7 @@
*/
/**
- * Processor leveraging WP_HTML_Tag_Processor which walks over a document and gathers breadcrumbs and invokes a callback for each open tag.
+ * Processor leveraging WP_HTML_Tag_Processor which gathers breadcrumbs which can be queried while iterating the open_tags() generator .
*
* @since n.e.x.t
* @access private
@@ -111,13 +111,14 @@ public function __construct( string $html ) {
}
/**
- * Walk over the document.
+ * Gets all open tags in the document.
*
- * Whenever an open tag is encountered, invoke the supplied $open_tag_callback and pass the tag name and breadcrumbs.
+ * A generator is used so that when iterating at a specific tag, additional information about the tag at that point
+ * can be queried from the class. Similarly, mutations may be performed when iterating at an open tag.
*
- * @param callable $open_tag_callback Open tag callback. The processor instance is passed as the sole argument.
+ * @return Generator Tag name of current open tag.
*/
- public function walk( callable $open_tag_callback ) {
+ public function open_tags(): Generator {
$p = $this->processor;
/*
@@ -170,8 +171,9 @@ public function walk( callable $open_tag_callback ) {
--$this->open_stack_indices[ $level ];
}
- // Invoke the callback to do processing.
- $open_tag_callback( $tag_name, $this->get_breadcrumbs() );
+ // Now that the breadcrumbs are constructed, yield the tag name so that they can be queried if desired.
+ // Other mutations may be performed to the open tag's attributes by the callee at this point as well.
+ yield $tag_name;
// Immediately pop off self-closing tags.
if ( in_array( $tag_name, self::SELF_CLOSING_TAGS, true ) ) {
@@ -214,20 +216,6 @@ public function walk( callable $open_tag_callback ) {
}
}
- /**
- * Returns the uppercase name of the matched tag.
- *
- * This is a wrapper around the underlying HTML_Tag_Processor method of the same name since only a limited number of
- * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
- *
- * @see WP_HTML_Tag_Processor::get_tag()
- *
- * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
- */
- public function get_tag() {
- return $this->processor->get_tag();
- }
-
/**
* Gets breadcrumbs for the current open tag.
*
diff --git a/modules/images/image-loading-optimization/optimization.php b/modules/images/image-loading-optimization/optimization.php
index 1e40e1ca87..206c351747 100644
--- a/modules/images/image-loading-optimization/optimization.php
+++ b/modules/images/image-loading-optimization/optimization.php
@@ -126,38 +126,36 @@ function ilo_optimize_template_output_buffer( string $buffer ): string {
// Walk over all IMG tags in the document and ensure fetchpriority is set/removed, and gather IMG attributes for preloading.
$processor = new ILO_HTML_Tag_Processor( $buffer );
- $processor->walk(
- static function () use ( $processor, $common_lcp_element, $all_breakpoints_have_url_metrics, &$lcp_elements_by_minimum_viewport_widths ) {
- if ( $processor->get_tag() !== 'IMG' ) {
- return;
- }
+ foreach ( $processor->open_tags() as $tag_name ) {
+ if ( 'IMG' !== $tag_name ) {
+ continue;
+ }
- // Ensure the fetchpriority attribute is set on the element properly.
- if ( $common_lcp_element && $processor->get_breadcrumbs() === $common_lcp_element['breadcrumbs'] ) {
- if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
- $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
- } else {
- $processor->set_attribute( 'fetchpriority', 'high' );
- $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
- }
- } elseif ( $all_breakpoints_have_url_metrics && $processor->get_attribute( 'fetchpriority' ) ) {
- // Note: The $all_breakpoints_have_url_metrics condition here allows for server-side heuristics to
- // continue to apply while waiting for all breakpoints to have metrics collected for them.
- $processor->set_attribute( 'data-ilo-removed-fetchpriority', $processor->get_attribute( 'fetchpriority' ) );
- $processor->remove_attribute( 'fetchpriority' );
+ // Ensure the fetchpriority attribute is set on the element properly.
+ if ( $common_lcp_element && $processor->get_breadcrumbs() === $common_lcp_element['breadcrumbs'] ) {
+ if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) {
+ $processor->set_attribute( 'data-ilo-fetchpriority-already-added', true );
+ } else {
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ $processor->set_attribute( 'data-ilo-added-fetchpriority', true );
}
+ } elseif ( $all_breakpoints_have_url_metrics && $processor->get_attribute( 'fetchpriority' ) ) {
+ // Note: The $all_breakpoints_have_url_metrics condition here allows for server-side heuristics to
+ // continue to apply while waiting for all breakpoints to have metrics collected for them.
+ $processor->set_attribute( 'data-ilo-removed-fetchpriority', $processor->get_attribute( 'fetchpriority' ) );
+ $processor->remove_attribute( 'fetchpriority' );
+ }
- // Capture the attributes from the LCP elements to use in preload links.
- foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
- if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
- $lcp_element['attributes'] = array();
- foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
- $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
- }
+ // Capture the attributes from the LCP elements to use in preload links.
+ foreach ( $lcp_elements_by_minimum_viewport_widths as &$lcp_element ) {
+ if ( $lcp_element && $lcp_element['breadcrumbs'] === $processor->get_breadcrumbs() ) {
+ $lcp_element['attributes'] = array();
+ foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin', 'integrity' ) as $attr_name ) {
+ $lcp_element['attributes'][ $attr_name ] = $processor->get_attribute( $attr_name );
}
}
}
- );
+ }
$buffer = $processor->get_updated_html();
// Inject any preload links at the end of the HEAD. In the future, WP_HTML_Processor could be used to do this injection.
From 3d7b5fae41d209b3dc1bcaa8d5cabcb36d480e8c Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Wed, 29 Nov 2023 20:43:32 -0800
Subject: [PATCH 123/371] Fix comment typo
Co-authored-by: Adam Silverstein
---
.../image-loading-optimization/class-ilo-html-tag-processor.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
index 9a4d373624..9b24fbbd94 100644
--- a/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
+++ b/modules/images/image-loading-optimization/class-ilo-html-tag-processor.php
@@ -133,7 +133,7 @@ public function open_tags(): Generator {
*