Skip to content

Commit

Permalink
Merge branch 'trunk' of https://github.com/WordPress/performance into…
Browse files Browse the repository at this point in the history
… add/fetchpriority-low
  • Loading branch information
westonruter committed Oct 18, 2024
2 parents 853f8e4 + 9e8b700 commit 05293de
Show file tree
Hide file tree
Showing 20 changed files with 449 additions and 78 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/js-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ jobs:
- name: npm install
run: npm ci
- name: JS Lint
run: npm run lint-js
run: npm run lint-js
- name: TypeScript compile
run: npm run tsc
4 changes: 3 additions & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const joinFiles = ( files ) => {
const PLUGIN_BASE_NAME = path.basename( __dirname );

module.exports = {
'**/*.js': ( files ) => `npm run lint-js -- ${ joinFiles( files ) }`,
'**/*.{js,ts}': ( files ) => {
return [ `npm run lint-js -- ${ joinFiles( files ) }`, `npm run tsc` ];
},
'**/*.php': ( files ) => {
const commands = [ 'composer phpstan' ];

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lodash": "4.17.21",
"micromatch": "^4.0.8",
"npm-run-all": "^4.1.5",
"typescript": "^5.6.3",
"webpackbar": "^6.0.1"
},
"scripts": {
Expand All @@ -46,6 +47,7 @@
"generate-pending-release-diffs": "bin/generate-pending-release-diffs.sh",
"format-js": "wp-scripts format",
"lint-js": "wp-scripts lint-js",
"tsc": "tsc",
"format-php": "composer format:all",
"phpstan": "composer phpstan",
"lint-php": "composer lint:all",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void {
$embed_wrapper_xpath = self::get_embed_wrapper_xpath( $processor->get_xpath() );

/**
* Collection of the minimum heights for the element with each group keyed by the minimum viewport with.
* Collection of the minimum heights for the element with each group keyed by the minimum viewport width.
*
* @var array<int, array{group: OD_URL_Metric_Group, height: int}> $minimums
*/
Expand Down
15 changes: 7 additions & 8 deletions plugins/embed-optimizer/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
const consoleLogPrefix = '[Embed Optimizer]';

/**
* @typedef {import("../optimization-detective/types.d.ts").ElementData} ElementMetrics
* @typedef {import("../optimization-detective/types.d.ts").URLMetric} URLMetric
* @typedef {import("../optimization-detective/types.d.ts").Extension} Extension
* @typedef {import("../optimization-detective/types.d.ts").InitializeCallback} InitializeCallback
* @typedef {import("../optimization-detective/types.d.ts").InitializeArgs} InitializeArgs
* @typedef {import("../optimization-detective/types.d.ts").FinalizeArgs} FinalizeArgs
* @typedef {import("../optimization-detective/types.d.ts").FinalizeCallback} FinalizeCallback
* @typedef {import("../optimization-detective/types.d.ts").ExtendedElementData} ExtendedElementData
* @typedef {import("../optimization-detective/types.ts").URLMetric} URLMetric
* @typedef {import("../optimization-detective/types.ts").Extension} Extension
* @typedef {import("../optimization-detective/types.ts").InitializeCallback} InitializeCallback
* @typedef {import("../optimization-detective/types.ts").InitializeArgs} InitializeArgs
* @typedef {import("../optimization-detective/types.ts").FinalizeArgs} FinalizeArgs
* @typedef {import("../optimization-detective/types.ts").FinalizeCallback} FinalizeCallback
* @typedef {import("../optimization-detective/types.ts").ExtendedElementData} ExtendedElementData
*/

/**
Expand Down
41 changes: 5 additions & 36 deletions plugins/embed-optimizer/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,44 +325,13 @@ function embed_optimizer_lazy_load_scripts(): void {
* @since 0.2.0
*/
function embed_optimizer_get_lazy_load_script(): string {
return <<<JS
const lazyEmbedsScripts = document.querySelectorAll( 'script[type="application/vnd.embed-optimizer.javascript"]' );
const lazyEmbedScriptsByParents = new Map();
$script = file_get_contents( __DIR__ . '/lazy-load.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request.

const lazyEmbedObserver = new IntersectionObserver(
( entries ) => {
for ( const entry of entries ) {
if ( entry.isIntersecting ) {
const lazyEmbedParent = entry.target;
const lazyEmbedScript = /** @type {HTMLScriptElement} */ lazyEmbedScriptsByParents.get( lazyEmbedParent );
const embedScript = document.createElement( 'script' );
for ( const attr of lazyEmbedScript.attributes ) {
if ( attr.nodeName === 'type' ) {
// Omit type=application/vnd.embed-optimizer.javascript type.
continue;
}
embedScript.setAttribute(
attr.nodeName === 'data-original-type' ? 'type' : attr.nodeName,
attr.nodeValue
);
}
lazyEmbedScript.replaceWith( embedScript );
lazyEmbedObserver.unobserve( lazyEmbedParent );
}
}
},
{
rootMargin: '100% 0% 100% 0%',
threshold: 0
}
);
if ( false === $script ) {
return '';
}

for ( const lazyEmbedScript of lazyEmbedsScripts ) {
const lazyEmbedParent = /** @type {HTMLElement} */ lazyEmbedScript.parentNode;
lazyEmbedScriptsByParents.set( lazyEmbedParent, lazyEmbedScript );
lazyEmbedObserver.observe( lazyEmbedParent );
}
JS;
return $script;
}

/**
Expand Down
55 changes: 55 additions & 0 deletions plugins/embed-optimizer/lazy-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Lazy load embeds
*
* When an embed block is lazy loaded, the script tag is replaced with a script tag that has the original attributes
*/

const lazyEmbedsScripts = document.querySelectorAll(
'script[type="application/vnd.embed-optimizer.javascript"]'
);
const lazyEmbedScriptsByParents = new Map();

const lazyEmbedObserver = new IntersectionObserver(
( entries ) => {
for ( const entry of entries ) {
if ( entry.isIntersecting ) {
const lazyEmbedParent = entry.target;
const lazyEmbedScript =
/** @type {HTMLScriptElement} */ lazyEmbedScriptsByParents.get(
lazyEmbedParent
);
const embedScript =
/** @type {HTMLScriptElement} */ document.createElement(
'script'
);
for ( const attr of lazyEmbedScript.attributes ) {
if ( attr.nodeName === 'type' ) {
// Omit type=application/vnd.embed-optimizer.javascript type.
continue;
}
embedScript.setAttribute(
attr.nodeName === 'data-original-type'
? 'type'
: attr.nodeName,
attr.nodeValue
);
}
lazyEmbedScript.replaceWith( embedScript );
lazyEmbedObserver.unobserve( lazyEmbedParent );
}
}
},
{
rootMargin: '100% 0% 100% 0%',
threshold: 0,
}
);

for ( const lazyEmbedScript of lazyEmbedsScripts ) {
const lazyEmbedParent =
/** @type {HTMLElement} */ lazyEmbedScript.parentNode;
if ( lazyEmbedParent instanceof HTMLElement ) {
lazyEmbedScriptsByParents.set( lazyEmbedParent, lazyEmbedScript );
lazyEmbedObserver.observe( lazyEmbedParent );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,93 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
return false;
}

// Skip empty poster attributes and data: URLs.
$poster = trim( (string) $processor->get_attribute( 'poster' ) );
// TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added.

$poster = $this->get_poster( $context );

if ( null !== $poster ) {
$this->reduce_poster_image_size( $poster, $context );
$this->preload_poster_image( $poster, $context );

return true;
}

return false;
}

/**
* Gets the poster from the current VIDEO element.
*
* Skips empty poster attributes and data: URLs.
*
* @since n.e.x.t
*
* @param OD_Tag_Visitor_Context $context Tag visitor context.
* @return non-empty-string|null Poster or null if not defined or is a data: URL.
*/
private function get_poster( OD_Tag_Visitor_Context $context ): ?string {
$poster = trim( (string) $context->processor->get_attribute( 'poster' ) );
if ( '' === $poster || $this->is_data_url( $poster ) ) {
return false;
return null;
}
return $poster;
}

/**
* Reduces poster image size by choosing one that fits the maximum video size more closely.
*
* @since n.e.x.t
*
* @param non-empty-string $poster Poster image URL.
* @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at a VIDEO tag.
*/
private function reduce_poster_image_size( string $poster, OD_Tag_Visitor_Context $context ): void {
$processor = $context->processor;

$xpath = $processor->get_xpath();

// TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added.
/*
* Obtain maximum width of the element exclusively from the URL metrics group with the widest viewport width,
* which would be desktop. This prevents the situation where if URL metrics have only so far been gathered for
* mobile viewports that an excessively-small poster would end up getting served to the first desktop visitor.
*/
$max_element_width = 0;
foreach ( $context->url_metric_group_collection->get_last_group() as $url_metric ) {
foreach ( $url_metric->get_elements() as $element ) {
if ( $element['xpath'] === $xpath ) {
$max_element_width = max( $max_element_width, $element['boundingClientRect']['width'] );
break; // Move on to the next URL Metric.
}
}
}

// If the element wasn't present in any URL Metrics gathered for desktop, then abort downsizing the poster.
if ( 0 === $max_element_width ) {
return;
}

$poster_id = attachment_url_to_postid( $poster );

if ( $poster_id > 0 ) {
$smaller_image_url = wp_get_attachment_image_url( $poster_id, array( (int) $max_element_width, 0 ) );
if ( is_string( $smaller_image_url ) ) {
$processor->set_attribute( 'poster', $smaller_image_url );
}
}
}

/**
* Preloads poster image for the LCP <video> element.
*
* @since n.e.x.t
*
* @param non-empty-string $poster Poster image URL.
* @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at a VIDEO tag.
*/
private function preload_poster_image( string $poster, OD_Tag_Visitor_Context $context ): void {
$processor = $context->processor;

$xpath = $processor->get_xpath();

// If this element is the LCP (for a breakpoint group), add a preload link for it.
foreach ( $context->url_metric_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) {
Expand All @@ -66,7 +144,5 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool {
$group->get_maximum_viewport_width()
);
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

$full_url = '';
$expected_url = '';

return array(
'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case, WP_UnitTest_Factory $factory ) use ( &$full_url, &$expected_url ): void {
$breakpoint_max_widths = array( 480, 600, 782 );
add_filter(
'od_breakpoint_max_widths',
static function () use ( $breakpoint_max_widths ) {
return $breakpoint_max_widths;
}
);

$element = array(
'isLCP' => false,
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]',
'boundingClientRect' => $test_case->get_sample_dom_rect(),
);

foreach ( array_merge( $breakpoint_max_widths, array( 1000 ) ) as $viewport_width ) {
OD_URL_Metrics_Post_Type::store_url_metric(
od_get_url_metrics_slug( od_get_normalized_query_vars() ),
$test_case->get_sample_url_metric(
array(
'viewport_width' => $viewport_width,
'elements' => array( $element ),
)
)
);
}

$attachment_id = $factory->attachment->create_object(
DIR_TESTDATA . '/images/33772.jpg',
0,
array(
'post_mime_type' => 'image/jpeg',
'post_excerpt' => 'A sample caption',
)
);

wp_generate_attachment_metadata( $attachment_id, DIR_TESTDATA . '/images/33772.jpg' );

$full_url = wp_get_attachment_url( $attachment_id );
$expected_url = wp_get_attachment_image_url( $attachment_id, array( (int) $element['boundingClientRect']['width'], 0 ) );
},
'buffer' => static function () use ( &$full_url ) {
return "
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<title>...</title>
</head>
<body>
<video class=\"desktop\" poster=\"$full_url\" width=\"1200\" height=\"500\" crossorigin>
<source src=\"https://example.com/header.webm\" type=\"video/webm\">
<source src=\"https://example.com/header.mp4\" type=\"video/mp4\">
</video>
</body>
</html>
";
},
'expected' => static function () use ( &$full_url, &$expected_url ) {
return "
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<title>...</title>
</head>
<body>
<video data-od-replaced-poster=\"$full_url\" data-od-xpath=\"/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]\" class=\"desktop\" poster=\"$expected_url\" width=\"1200\" height=\"500\" crossorigin>
<source src=\"https://example.com/header.webm\" type=\"video/webm\">
<source src=\"https://example.com/header.mp4\" type=\"video/mp4\">
</video>
<script type=\"module\">/* import detect ... */</script>
</body>
</html>
";
},
);
Loading

0 comments on commit 05293de

Please sign in to comment.