Skip to content

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

Draft
wants to merge 16 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function perflab_aea_enqueued_js_assets_test(): array {
}

$result = array(
'label' => __( 'Enqueued scripts', 'performance-lab' ),
'label' => __( 'Blocking scripts', 'performance-lab' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Performance', 'performance-lab' ),
Expand All @@ -45,8 +45,8 @@ function perflab_aea_enqueued_js_assets_test(): array {
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'
),
Expand Down Expand Up @@ -86,8 +86,8 @@ function perflab_aea_enqueued_js_assets_test(): array {
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'
),
Expand Down Expand Up @@ -128,7 +128,7 @@ function perflab_aea_enqueued_css_assets_test(): array {
return array( 'omitted' => true );
}
$result = array(
'label' => __( 'Enqueued styles', 'performance-lab' ),
'label' => __( 'Blocking styles', 'performance-lab' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Performance', 'performance-lab' ),
Expand All @@ -140,8 +140,8 @@ function perflab_aea_enqueued_css_assets_test(): array {
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'
),
Expand Down Expand Up @@ -180,8 +180,8 @@ function perflab_aea_enqueued_css_assets_test(): array {
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'
),
Expand Down Expand Up @@ -212,10 +212,10 @@ function perflab_aea_enqueued_css_assets_test(): array {
* @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;
}
Expand All @@ -228,11 +228,11 @@ function perflab_aea_get_total_enqueued_scripts() {
* @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'];
}
Expand All @@ -249,10 +249,10 @@ function perflab_aea_get_total_size_bytes_enqueued_scripts() {
* @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;
}
Expand All @@ -265,11 +265,11 @@ function perflab_aea_get_total_enqueued_styles() {
* @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'];
}
Expand All @@ -293,6 +293,11 @@ function perflab_aea_get_path_from_resource_url( string $resource_url ): string
return '';
}

// Remove query string if present.
if ( false !== strpos( $resource_url, '?' ) ) {
$resource_url = substr( $resource_url, 0, strpos( $resource_url, '?' ) );
Comment on lines +296 to +298
Copy link
Contributor Author

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?

/**
* Convert full URL paths to absolute paths.
* Covers Standard WP configuration, wp-content outside WP directories and subdirectories.
* Ex: https://example.com/content/themes/, https://example.com/wp/wp-includes/
*
* @since 1.0.0
*
* @param string $resource_url URl resource link.
* @return string Returns absolute path to the resource.
*/
function perflab_aea_get_path_from_resource_url( string $resource_url ): string {

Copy link
Member

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.

}
Comment on lines +297 to +299
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( false !== strpos( $resource_url, '?' ) ) {
$resource_url = substr( $resource_url, 0, strpos( $resource_url, '?' ) );
}
$resource_url = preg_replace( '/\?.*/', '', $resource_url );

Nevertheless, the perflab_aea_get_path_from_resource_url() function isn't being used now anymore, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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() ) );
Expand All @@ -307,3 +312,28 @@ function perflab_aea_get_path_from_resource_url( string $resource_url ): string
// 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 ) && isset( $content_length[0] ) ) {
$content_length = $content_length[0];
}
$content_length = (int) $content_length;
if ( $content_length <= 0 ) {
return null;
}
return $content_length;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 ( 'module' === strtolower( (string) $type ) ) {
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 ( '' !== $type && 'text/javascript' !== strtolower( (string) $type ) ) {
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 ( 'stylesheet' !== strtolower( (string) $rel ) ) {
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.
Expand Down Expand Up @@ -158,6 +155,8 @@ function perflab_aea_clean_aea_audit_action(): void {
* @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' );
}
Expand Down
Loading
Loading