-
Notifications
You must be signed in to change notification settings - Fork 128
Use HTML Tag Processor to audit blocking scripts & styles in Site Health’s enqueued-assets test #2059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Use HTML Tag Processor to audit blocking scripts & styles in Site Health’s enqueued-assets test #2059
Changes from 15 commits
1e60c9a
8baf9af
0f4b4ba
4a37fff
8e5b9dc
d53ad9b
3bdae33
2846222
668808a
40377b8
092ef31
84692cf
f341208
12d1cc7
e58e451
f83151b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -33,7 +33,7 @@ | |||||||||
} | ||||||||||
|
||||||||||
$result = array( | ||||||||||
'label' => __( 'Enqueued scripts', 'performance-lab' ), | ||||||||||
'label' => __( 'Blocking scripts', 'performance-lab' ), | ||||||||||
'status' => 'good', | ||||||||||
'badge' => array( | ||||||||||
'label' => __( 'Performance', 'performance-lab' ), | ||||||||||
|
@@ -45,8 +45,8 @@ | |||||||||
sprintf( | ||||||||||
/* translators: 1: Number of enqueued styles. 2.Styles size. */ | ||||||||||
_n( | ||||||||||
'The amount of %1$s enqueued script (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s enqueued scripts (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s blocking script (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s blocking scripts (size: %2$s) is acceptable.', | ||||||||||
$enqueued_scripts, | ||||||||||
'performance-lab' | ||||||||||
), | ||||||||||
|
@@ -86,8 +86,8 @@ | |||||||||
sprintf( | ||||||||||
/* translators: 1: Number of enqueued styles. 2.Styles size. */ | ||||||||||
_n( | ||||||||||
'Your website enqueues %1$s script (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website enqueues %1$s scripts (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website has %1$s blocking script (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website has %1$s blocking scripts (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
$enqueued_scripts, | ||||||||||
'performance-lab' | ||||||||||
), | ||||||||||
|
@@ -128,7 +128,7 @@ | |||||||||
return array( 'omitted' => true ); | ||||||||||
} | ||||||||||
$result = array( | ||||||||||
'label' => __( 'Enqueued styles', 'performance-lab' ), | ||||||||||
'label' => __( 'Blocking styles', 'performance-lab' ), | ||||||||||
'status' => 'good', | ||||||||||
'badge' => array( | ||||||||||
'label' => __( 'Performance', 'performance-lab' ), | ||||||||||
|
@@ -140,8 +140,8 @@ | |||||||||
sprintf( | ||||||||||
/* translators: 1: Number of enqueued styles. 2.Styles size. */ | ||||||||||
_n( | ||||||||||
'The amount of %1$s enqueued style (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s enqueued styles (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s blocking style (size: %2$s) is acceptable.', | ||||||||||
'The amount of %1$s blocking styles (size: %2$s) is acceptable.', | ||||||||||
$enqueued_styles, | ||||||||||
'performance-lab' | ||||||||||
), | ||||||||||
|
@@ -180,8 +180,8 @@ | |||||||||
sprintf( | ||||||||||
/* translators: 1: Number of enqueued styles. 2.Styles size. */ | ||||||||||
_n( | ||||||||||
'Your website enqueues %1$s style (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website enqueues %1$s styles (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website has %1$s blocking style (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
'Your website has %1$s blocking styles (size: %2$s). Try to reduce the number or to concatenate them.', | ||||||||||
$enqueued_styles, | ||||||||||
'performance-lab' | ||||||||||
), | ||||||||||
|
@@ -212,10 +212,10 @@ | |||||||||
* @return int|false Number of total scripts or false if transient hasn't been set. | ||||||||||
*/ | ||||||||||
function perflab_aea_get_total_enqueued_scripts() { | ||||||||||
$enqueued_scripts = false; | ||||||||||
$list_enqueued_scripts = get_transient( 'aea_enqueued_front_page_scripts' ); | ||||||||||
if ( is_array( $list_enqueued_scripts ) ) { | ||||||||||
$enqueued_scripts = count( $list_enqueued_scripts ); | ||||||||||
$enqueued_scripts = false; | ||||||||||
$blocking_assets = get_transient( 'aea_blocking_assets' ); | ||||||||||
if ( is_array( $blocking_assets ) && is_array( $blocking_assets['scripts'] ) ) { | ||||||||||
$enqueued_scripts = count( $blocking_assets['scripts'] ); | ||||||||||
} | ||||||||||
return $enqueued_scripts; | ||||||||||
} | ||||||||||
|
@@ -228,11 +228,11 @@ | |||||||||
* @return int|false Byte Total size or false if transient hasn't been set. | ||||||||||
*/ | ||||||||||
function perflab_aea_get_total_size_bytes_enqueued_scripts() { | ||||||||||
$total_size = false; | ||||||||||
$list_enqueued_scripts = get_transient( 'aea_enqueued_front_page_scripts' ); | ||||||||||
if ( is_array( $list_enqueued_scripts ) ) { | ||||||||||
$total_size = false; | ||||||||||
$blocking_assets = get_transient( 'aea_blocking_assets' ); | ||||||||||
if ( is_array( $blocking_assets ) && isset( $blocking_assets['scripts'] ) && is_array( $blocking_assets['scripts'] ) ) { | ||||||||||
$total_size = 0; | ||||||||||
foreach ( $list_enqueued_scripts as $enqueued_script ) { | ||||||||||
foreach ( $blocking_assets['scripts'] as $enqueued_script ) { | ||||||||||
if ( is_array( $enqueued_script ) && array_key_exists( 'size', $enqueued_script ) && is_int( $enqueued_script['size'] ) ) { | ||||||||||
$total_size += $enqueued_script['size']; | ||||||||||
} | ||||||||||
|
@@ -249,10 +249,10 @@ | |||||||||
* @return int|false Number of total styles or false if transient hasn't been set. | ||||||||||
*/ | ||||||||||
function perflab_aea_get_total_enqueued_styles() { | ||||||||||
$enqueued_styles = false; | ||||||||||
$list_enqueued_styles = get_transient( 'aea_enqueued_front_page_styles' ); | ||||||||||
if ( is_array( $list_enqueued_styles ) ) { | ||||||||||
$enqueued_styles = count( $list_enqueued_styles ); | ||||||||||
$enqueued_styles = false; | ||||||||||
$blocking_assets = get_transient( 'aea_blocking_assets' ); | ||||||||||
if ( is_array( $blocking_assets ) && isset( $blocking_assets['styles'] ) && is_array( $blocking_assets['styles'] ) ) { | ||||||||||
$enqueued_styles = count( $blocking_assets['styles'] ); | ||||||||||
} | ||||||||||
return $enqueued_styles; | ||||||||||
} | ||||||||||
|
@@ -265,11 +265,11 @@ | |||||||||
* @return int|false Byte Total size or false if transient hasn't been set. | ||||||||||
*/ | ||||||||||
function perflab_aea_get_total_size_bytes_enqueued_styles() { | ||||||||||
$total_size = false; | ||||||||||
$list_enqueued_styles = get_transient( 'aea_enqueued_front_page_styles' ); | ||||||||||
if ( is_array( $list_enqueued_styles ) ) { | ||||||||||
$total_size = false; | ||||||||||
$blocking_assets = get_transient( 'aea_blocking_assets' ); | ||||||||||
if ( is_array( $blocking_assets ) && isset( $blocking_assets['styles'] ) && is_array( $blocking_assets['styles'] ) ) { | ||||||||||
$total_size = 0; | ||||||||||
foreach ( $list_enqueued_styles as $enqueued_style ) { | ||||||||||
foreach ( $blocking_assets['styles'] as $enqueued_style ) { | ||||||||||
if ( is_array( $enqueued_style ) && array_key_exists( 'size', $enqueued_style ) && is_int( $enqueued_style['size'] ) ) { | ||||||||||
$total_size += $enqueued_style['size']; | ||||||||||
} | ||||||||||
|
@@ -293,6 +293,11 @@ | |||||||||
return ''; | ||||||||||
} | ||||||||||
|
||||||||||
// Remove query string if present. | ||||||||||
if ( false !== strpos( $resource_url, '?' ) ) { | ||||||||||
$resource_url = substr( $resource_url, 0, strpos( $resource_url, '?' ) ); | ||||||||||
} | ||||||||||
Comment on lines
+297
to
+299
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nevertheless, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes its not being used anymore I just put comment about it here #2059 (comment). |
||||||||||
|
||||||||||
// Different content folder ex. /content/. | ||||||||||
if ( 0 === strpos( $resource_url, content_url() ) ) { | ||||||||||
return WP_CONTENT_DIR . substr( $resource_url, strlen( content_url() ) ); | ||||||||||
|
@@ -307,3 +312,28 @@ | |||||||||
// Standard wp-content configuration. | ||||||||||
return untrailingslashit( ABSPATH ) . wp_make_link_relative( $resource_url ); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Gets the content length of an asset. | ||||||||||
* | ||||||||||
* @since n.e.x.t | ||||||||||
* | ||||||||||
* @param string $resource_url URL of the resource. | ||||||||||
* @return int|null Returns the content length in bytes or null if it cannot be determined. | ||||||||||
*/ | ||||||||||
function perflab_aea_get_asset_content_length( string $resource_url ): ?int { | ||||||||||
$head_response = wp_remote_head( $resource_url, array( 'timeout' => 10 ) ); | ||||||||||
if ( is_wp_error( $head_response ) || 200 !== wp_remote_retrieve_response_code( $head_response ) ) { | ||||||||||
return null; | ||||||||||
} | ||||||||||
|
||||||||||
$content_length = wp_remote_retrieve_header( $head_response, 'content-length' ); | ||||||||||
if ( is_array( $content_length ) && 0 < count( $content_length ) ) { | ||||||||||
b1ink0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
$content_length = $content_length[0]; | ||||||||||
} | ||||||||||
if ( ! is_string( $content_length ) || '' === $content_length || ! ctype_digit( $content_length ) || 0 === (int) $content_length ) { | ||||||||||
return null; | ||||||||||
} | ||||||||||
|
||||||||||
return (int) $content_length; | ||||||||||
b1ink0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,107 +13,104 @@ | |||||||||
// @codeCoverageIgnoreEnd | ||||||||||
|
||||||||||
/** | ||||||||||
* Audit enqueued and printed scripts in is_front_page(). Ignore /wp-includes scripts. | ||||||||||
* Audit blocking assets on the front page. | ||||||||||
* | ||||||||||
* It will save information in a transient for 12 hours. | ||||||||||
* | ||||||||||
* @since 1.0.0 | ||||||||||
* | ||||||||||
* @global WP_Scripts $wp_scripts | ||||||||||
* @since n.e.x.t | ||||||||||
*/ | ||||||||||
function perflab_aea_audit_enqueued_scripts(): void { | ||||||||||
if ( ! is_admin() && is_front_page() && current_user_can( 'view_site_health_checks' ) && false === get_transient( 'aea_enqueued_front_page_scripts' ) ) { | ||||||||||
global $wp_scripts; | ||||||||||
$enqueued_scripts = array(); | ||||||||||
function perflab_aea_audit_blocking_assets(): void { | ||||||||||
if ( | ||||||||||
! is_admin() || | ||||||||||
! current_user_can( 'view_site_health_checks' ) || | ||||||||||
false !== get_transient( 'aea_blocking_assets' ) | ||||||||||
) { | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
$response = wp_remote_get( | ||||||||||
home_url( '/' ), | ||||||||||
array( | ||||||||||
'timeout' => 10, | ||||||||||
'headers' => array( | ||||||||||
'Accept' => 'text/html', | ||||||||||
'Cache-Control' => 'no-cache', | ||||||||||
), | ||||||||||
Comment on lines
+33
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the site has HTTP Basic auth, then these can be copied in the request headers. This is done in core in the plugin/theme file editors. |
||||||||||
) | ||||||||||
); | ||||||||||
|
||||||||||
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
foreach ( $wp_scripts->done as $handle ) { | ||||||||||
$script = $wp_scripts->registered[ $handle ]; | ||||||||||
$html = wp_remote_retrieve_body( $response ); | ||||||||||
if ( '' === $html ) { | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
$assets = array( | ||||||||||
'scripts' => array(), | ||||||||||
'styles' => array(), | ||||||||||
); | ||||||||||
|
||||||||||
$processor = new WP_HTML_Tag_Processor( $html ); | ||||||||||
|
||||||||||
if ( ! $script->src || false !== strpos( $script->src, 'wp-includes' ) ) { | ||||||||||
while ( $processor->next_tag() ) { | ||||||||||
$tag = $processor->get_tag(); | ||||||||||
|
||||||||||
if ( 'SCRIPT' === $tag ) { | ||||||||||
$src = $processor->get_attribute( 'src' ); | ||||||||||
if ( ! is_string( $src ) ) { | ||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
// Add any extra data (inlined) that was passed with the script. | ||||||||||
$inline_size = 0; | ||||||||||
if ( | ||||||||||
isset( $script->extra['after'] ) && | ||||||||||
is_array( $script->extra['after'] ) | ||||||||||
) { | ||||||||||
foreach ( $script->extra['after'] as $extra ) { | ||||||||||
$inline_size += ( is_string( $extra ) ) ? mb_strlen( $extra, '8bit' ) : 0; | ||||||||||
} | ||||||||||
} | ||||||||||
// Note that when the "type" attribute is absent or empty, the element is treated as a classic JavaScript script. | ||||||||||
$type = $processor->get_attribute( 'type' ); | ||||||||||
|
||||||||||
$path = perflab_aea_get_path_from_resource_url( $script->src ); | ||||||||||
if ( '' === $path ) { | ||||||||||
// Skip external script with "async" or "defer" attributes. | ||||||||||
if ( null !== $processor->get_attribute( 'async' ) || null !== $processor->get_attribute( 'defer' ) ) { | ||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
$enqueued_scripts[] = array( | ||||||||||
'src' => $script->src, | ||||||||||
'size' => wp_filesize( $path ) + $inline_size, | ||||||||||
); | ||||||||||
|
||||||||||
} | ||||||||||
set_transient( 'aea_enqueued_front_page_scripts', $enqueued_scripts, 12 * HOUR_IN_SECONDS ); | ||||||||||
} | ||||||||||
} | ||||||||||
add_action( 'wp_footer', 'perflab_aea_audit_enqueued_scripts', PHP_INT_MAX ); | ||||||||||
// Skip external script with a "type" attribute set to "module" as they are deferred by default. | ||||||||||
if ( is_string( $type ) && '' !== $type && 'module' === strtolower( $type ) ) { | ||||||||||
b1ink0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Audit enqueued and printed styles in the frontend. Ignore /wp-includes styles. | ||||||||||
* | ||||||||||
* It will save information in a transient for 12 hours. | ||||||||||
* | ||||||||||
* @since 1.0.0 | ||||||||||
* | ||||||||||
* @global WP_Styles $wp_styles The WP_Styles current instance. | ||||||||||
*/ | ||||||||||
function perflab_aea_audit_enqueued_styles(): void { | ||||||||||
if ( ! is_admin() && is_front_page() && current_user_can( 'view_site_health_checks' ) && false === get_transient( 'aea_enqueued_front_page_styles' ) ) { | ||||||||||
global $wp_styles; | ||||||||||
$enqueued_styles = array(); | ||||||||||
foreach ( $wp_styles->done as $handle ) { | ||||||||||
$style = $wp_styles->registered[ $handle ]; | ||||||||||
|
||||||||||
if ( ! $style->src || false !== strpos( $style->src, 'wp-includes' ) ) { | ||||||||||
// Skip external script with a "type" attribute that is not JavaScript. | ||||||||||
if ( is_string( $type ) && '' !== $type && 'text/javascript' !== strtolower( $type ) ) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, it is necessary to keep Then it can be done like this:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, this change breaks things because the which check would be better? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, this is executed: <script type>document.write('hello');</script> There's also this code in core which is relevant here: With the HTML API, the tag would be JavaScript if:
|
||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
// Check if we already have the style's path ( part of a refactor for block styles from 5.9 ). | ||||||||||
if ( | ||||||||||
isset( $style->extra['path'] ) && | ||||||||||
is_string( $style->extra['path'] ) && | ||||||||||
'' !== $style->extra['path'] | ||||||||||
) { | ||||||||||
$path = $style->extra['path']; | ||||||||||
} else { // Fallback to getting the path from the style's src. | ||||||||||
$path = perflab_aea_get_path_from_resource_url( $style->src ); | ||||||||||
if ( '' === $path ) { | ||||||||||
continue; | ||||||||||
} | ||||||||||
$size = perflab_aea_get_asset_content_length( $src ); | ||||||||||
if ( null !== $size ) { | ||||||||||
$assets['scripts'][] = array( | ||||||||||
'src' => $src, | ||||||||||
'size' => $size, | ||||||||||
); | ||||||||||
} | ||||||||||
} elseif ( 'LINK' === $tag ) { | ||||||||||
$rel = $processor->get_attribute( 'rel' ); | ||||||||||
if ( ! is_string( $rel ) || 'stylesheet' !== strtolower( $rel ) ) { | ||||||||||
b1ink0 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is another special case to consider here, and that is a classic hack for non-blocking stylesheets. This is commonly implemented by adding https://www.filamentgroup.com/lab/load-css-simpler/ So this case can be accounted for as well. Namely, this can just skip considering any stylesheets which have a |
||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
// Add any extra data (inlined) that was passed with the style. | ||||||||||
$inline_size = 0; | ||||||||||
if ( | ||||||||||
isset( $style->extra['after'] ) && | ||||||||||
is_array( $style->extra['after'] ) | ||||||||||
) { | ||||||||||
foreach ( $style->extra['after'] as $extra ) { | ||||||||||
$inline_size += ( is_string( $extra ) ) ? mb_strlen( $extra, '8bit' ) : 0; | ||||||||||
} | ||||||||||
$href = $processor->get_attribute( 'href' ); | ||||||||||
if ( ! is_string( $href ) ) { | ||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
$enqueued_styles[] = array( | ||||||||||
'src' => $style->src, | ||||||||||
'size' => wp_filesize( $path ) + $inline_size, | ||||||||||
); | ||||||||||
$size = perflab_aea_get_asset_content_length( $href ); | ||||||||||
if ( null !== $size ) { | ||||||||||
$assets['styles'][] = array( | ||||||||||
'src' => $href, | ||||||||||
'size' => $size, | ||||||||||
); | ||||||||||
} | ||||||||||
} | ||||||||||
set_transient( 'aea_enqueued_front_page_styles', $enqueued_styles, 12 * HOUR_IN_SECONDS ); | ||||||||||
} | ||||||||||
|
||||||||||
set_transient( 'aea_blocking_assets', $assets, 12 * HOUR_IN_SECONDS ); | ||||||||||
} | ||||||||||
add_action( 'wp_footer', 'perflab_aea_audit_enqueued_styles', PHP_INT_MAX ); | ||||||||||
add_action( 'admin_init', 'perflab_aea_audit_blocking_assets' ); | ||||||||||
|
||||||||||
/** | ||||||||||
* Adds tests to site health. | ||||||||||
|
@@ -158,6 +155,8 @@ | |||||||||
* @since 1.0.0 | ||||||||||
*/ | ||||||||||
function perflab_aea_invalidate_cache_transients(): void { | ||||||||||
delete_transient( 'aea_blocking_assets' ); | ||||||||||
// Keeping legacy transients deletion for backward compatibility. | ||||||||||
delete_transient( 'aea_enqueued_front_page_scripts' ); | ||||||||||
delete_transient( 'aea_enqueued_front_page_styles' ); | ||||||||||
} | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to remove the function
perflab_aea_get_path_from_resource_url
entirely, as a HEAD request is now made for local assets as well?performance/plugins/performance-lab/includes/site-health/audit-enqueued-assets/helper.php
Lines 281 to 291 in 8856a14
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think the function can be removed.