Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dbcd32c
Output script modules with fetchpriority=low
westonruter May 19, 2025
5207c27
Fix duplicate array keys in data_special_chars_script_encoding
westonruter May 19, 2025
088366d
Add ability to set fetch priority for script modules
westonruter May 19, 2025
9eab85d
Add fetchpriority support for non-module scripts
westonruter May 19, 2025
d9c4037
Set fetchpriority=low on comment-reply script
westonruter May 19, 2025
4f82a4f
Use auto as default fetchpriority for script modules
westonruter May 19, 2025
f2cd9be
Avoid printing fetchpriority attribute when auto
westonruter May 19, 2025
ff4fd36
fixup! Use auto as default fetchpriority for script modules
westonruter May 19, 2025
9edbe4c
Use HTML Tag Processor to parse import map
westonruter May 19, 2025
021fe74
Ensure parity in args between class methods and global function aliases
westonruter May 19, 2025
34ef7fa
Use fetch priority low by default for Interactivity API view script m…
westonruter May 20, 2025
e54274a
Add missing since tag
westonruter May 21, 2025
cc1d909
Account for full block.json schema when checking for interactivity
westonruter May 21, 2025
4197eb2
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Jun 20, 2025
74a49e1
Remove PHPStan annotations for commit
westonruter Jun 20, 2025
75fb879
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Jul 29, 2025
0e9994a
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Aug 26, 2025
06e9cac
Remove TODO comments which have been filed in https://github.com/Word…
westonruter Aug 26, 2025
f91c61d
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Sep 1, 2025
4596644
Add validation for fetchpriority set on scripts
westonruter Sep 2, 2025
2172e9e
Allow empty string for set_fetchpriority(), return bool, and add tests
westonruter Sep 2, 2025
535669d
Add missing type for args param
westonruter Sep 2, 2025
fe366e2
Improve typing for is_delayed_stragegy
westonruter Sep 2, 2025
f704f5e
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Sep 2, 2025
38150dc
Replace n.e.x.t with 6.9.0
westonruter Sep 2, 2025
f05ca72
Ensure reflection property is accessible
westonruter Sep 2, 2025
607ce0c
Use static data provider methods
westonruter Sep 3, 2025
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
12 changes: 11 additions & 1 deletion src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,21 @@ function register_block_script_module_id( $metadata, $field_name, $index = 0 ) {
$block_version = isset( $metadata['version'] ) ? $metadata['version'] : false;
$module_version = isset( $module_asset['version'] ) ? $module_asset['version'] : $block_version;

// Blocks using the Interactivity API are server-side rendered, so they are by design not in the critical rendering path and should be deprioritized.
$args = array();
if (
( isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity'] ) ||
( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] )
) {
$args['fetchpriority'] = 'low';
}

wp_register_script_module(
$module_id,
$module_uri,
$module_dependencies,
$module_version
$module_version,
$args
);

return $module_id;
Expand Down
112 changes: 95 additions & 17 deletions src/wp-includes/class-wp-script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class WP_Script_Modules {
* identifier has already been registered.
*
* @since 6.5.0
* @since 6.9.0 Added the $args parameter.
*
* @param string $id The identifier of the script module. Should be unique. It will be used in the
* final import map.
Expand All @@ -71,13 +72,18 @@ class WP_Script_Modules {
* It is added to the URL as a query string for cache busting purposes. If $version
* is set to false, the version number is the currently installed WordPress version.
* If $version is set to null, no version is added.
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
public function register( string $id, string $src, array $deps = array(), $version = false ) {
public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
if ( ! isset( $this->registered[ $id ] ) ) {
$dependencies = array();
foreach ( $deps as $dependency ) {
if ( is_array( $dependency ) ) {
if ( ! isset( $dependency['id'] ) ) {
if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) {
_doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
continue;
}
Expand All @@ -95,13 +101,76 @@ public function register( string $id, string $src, array $deps = array(), $versi
}
}

$fetchpriority = 'auto';
if ( isset( $args['fetchpriority'] ) ) {
if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
$fetchpriority = $args['fetchpriority'];
} else {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: $fetchpriority, 2: $id */
__( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ),
$id
),
'6.9.0'
);
}
}

$this->registered[ $id ] = array(
'src' => $src,
'version' => $version,
'enqueue' => isset( $this->enqueued_before_registered[ $id ] ),
'dependencies' => $dependencies,
'src' => $src,
'version' => $version,
'enqueue' => isset( $this->enqueued_before_registered[ $id ] ),
'dependencies' => $dependencies,
'fetchpriority' => $fetchpriority,
);
}
}

/**
* Checks if the provided fetchpriority is valid.
*
* @since 6.9.0
*
* @param string|mixed $priority Fetch priority.
* @return bool Whether valid fetchpriority.
*/
private function is_valid_fetchpriority( $priority ): bool {
return in_array( $priority, array( 'auto', 'low', 'high' ), true );
}

/**
* Sets the fetch priority for a script module.
*
* @since 6.9.0
*
* @param string $id Script module identifier.
* @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
* @return bool Whether setting the fetchpriority was successful.
*/
public function set_fetchpriority( string $id, string $priority ): bool {
if ( ! isset( $this->registered[ $id ] ) ) {
return false;
}

if ( '' === $priority ) {
$priority = 'auto';
}

if ( ! $this->is_valid_fetchpriority( $priority ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Invalid fetchpriority. */
sprintf( __( 'Invalid fetchpriority: %s' ), $priority ),
'6.9.0'
);
return false;
}

$this->registered[ $id ]['fetchpriority'] = $priority;
return true;
}

/**
Expand All @@ -111,6 +180,7 @@ public function register( string $id, string $src, array $deps = array(), $versi
* will be registered.
*
* @since 6.5.0
* @since 6.9.0 Added the $args parameter.
*
* @param string $id The identifier of the script module. Should be unique. It will be used in the
* final import map.
Expand All @@ -136,12 +206,17 @@ public function register( string $id, string $src, array $deps = array(), $versi
* It is added to the URL as a query string for cache busting purposes. If $version
* is set to false, the version number is the currently installed WordPress version.
* If $version is set to null, no version is added.
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) {
public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
if ( isset( $this->registered[ $id ] ) ) {
$this->registered[ $id ]['enqueue'] = true;
} elseif ( $src ) {
$this->register( $id, $src, $deps, $version );
$this->register( $id, $src, $deps, $version, $args );
$this->registered[ $id ]['enqueue'] = true;
} else {
$this->enqueued_before_registered[ $id ] = true;
Expand Down Expand Up @@ -208,13 +283,15 @@ public function add_hooks() {
*/
public function print_enqueued_script_modules() {
foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
wp_print_script_tag(
array(
'type' => 'module',
'src' => $this->get_src( $id ),
'id' => $id . '-js-module',
)
$args = array(
'type' => 'module',
'src' => $this->get_src( $id ),
'id' => $id . '-js-module',
);
if ( 'auto' !== $script_module['fetchpriority'] ) {
$args['fetchpriority'] = $script_module['fetchpriority'];
}
wp_print_script_tag( $args );
}
}

Expand All @@ -231,9 +308,10 @@ public function print_script_module_preloads() {
// Don't preload if it's marked for enqueue.
if ( true !== $script_module['enqueue'] ) {
echo sprintf(
'<link rel="modulepreload" href="%s" id="%s">',
'<link rel="modulepreload" href="%s" id="%s"%s>',
esc_url( $this->get_src( $id ) ),
esc_attr( $id . '-js-modulepreload' )
esc_attr( $id . '-js-modulepreload' ),
'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : ''
);
}
}
Expand Down Expand Up @@ -278,7 +356,7 @@ private function get_import_map(): array {
*
* @since 6.5.0
*
* @return array[] Script modules marked for enqueue, keyed by script module identifier.
* @return array<string, array> Script modules marked for enqueue, keyed by script module identifier.
*/
private function get_marked_for_enqueue(): array {
$enqueued = array();
Expand Down
48 changes: 46 additions & 2 deletions src/wp-includes/class-wp-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ public function do_item( $handle, $group = false ) {
if ( $intended_strategy ) {
$attr['data-wp-strategy'] = $intended_strategy;
}
if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) {
$attr['fetchpriority'] = $obj->extra['fetchpriority'];
}
$tag = $translations . $ie_conditional_prefix . $before_script;
$tag .= wp_get_script_tag( $attr );
$tag .= $after_script . $ie_conditional_suffix;
Expand Down Expand Up @@ -831,6 +834,35 @@ public function add_data( $handle, $key, $value ) {
);
return false;
}
} elseif ( 'fetchpriority' === $key ) {
if ( empty( $value ) ) {
$value = 'auto';
}
if ( ! $this->is_valid_fetchpriority( $value ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: $fetchpriority, 2: $handle */
__( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
is_string( $value ) ? $value : gettype( $value ),
$handle
),
'6.9.0'
);
return false;
} elseif ( ! $this->registered[ $handle ]->src ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: $fetchpriority, 2: $handle */
__( 'Cannot supply a fetchpriority `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ),
is_string( $value ) ? $value : gettype( $value ),
$handle
),
'6.9.0'
);
return false;
}
}
return parent::add_data( $handle, $key, $value );
}
Expand Down Expand Up @@ -869,17 +901,29 @@ private function get_dependents( $handle ) {
*
* @since 6.3.0
*
* @param string $strategy The strategy to check.
* @param string|mixed $strategy The strategy to check.
* @return bool True if $strategy is one of the delayed strategies, otherwise false.
*/
private function is_delayed_strategy( $strategy ) {
private function is_delayed_strategy( $strategy ): bool {
return in_array(
$strategy,
$this->delayed_strategies,
true
);
}

/**
* Checks if the provided fetchpriority is valid.
*
* @since 6.9.0
*
* @param string|mixed $priority Fetch priority.
* @return bool Whether valid fetchpriority.
*/
private function is_valid_fetchpriority( $priority ): bool {
return in_array( $priority, array( 'auto', 'low', 'high' ), true );
}

/**
* Gets the best eligible loading strategy for a script.
*
Expand Down
18 changes: 14 additions & 4 deletions src/wp-includes/functions.wp-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) {
* @since 2.1.0
* @since 4.3.0 A return value was added.
* @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array.
* @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array.
*
* @param string $handle Name of the script. Should be unique.
* @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory.
Expand All @@ -171,8 +172,9 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'.
* }
* @return bool Whether the script has been registered. True on success, false on failure.
*/
Expand All @@ -193,6 +195,9 @@ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args
if ( ! empty( $args['strategy'] ) ) {
$wp_scripts->add_data( $handle, 'strategy', $args['strategy'] );
}
if ( ! empty( $args['fetchpriority'] ) ) {
$wp_scripts->add_data( $handle, 'fetchpriority', $args['fetchpriority'] );
Copy link
Member

Choose a reason for hiding this comment

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

The fetchpriority is checked against an allowlist for modules. I don't see that for scripts. Did I overlook that or should it be included?

Copy link
Member Author

Choose a reason for hiding this comment

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

@sirreal In the case of classic scripts, the fetchpriority is instead being specified when the scripts are registered. The only script which gets low is comment-reply:

$scripts->add_data( 'comment-reply', 'fetchpriority', 'low' ); // In Chrome this is automatically low due to the async strategy, but in Firefox and Safari the priority is normal/medium.

There isn't any allowlist to default to low for non-module scripts since there is no generalizable pattern for being able to opt in like there is for script modules with the Interactivity API.

Copy link
Member

Choose a reason for hiding this comment

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

More than default I'm wondering about accepting arbitrary values. It's not likely harmful, but perhaps _doing_it_wrong could be shown and the value discarded if an unsupported fetchpriority is provided.

I understand it's a well defined set of values: high | low | auto.

For example:

wp_enqueue_script( 'example', '/ex.js', array(), '' );
wp_scripts()->add_data( 'example', 'fetchpriority', 'silly' );

produces

<script src="/ex.js?ver=…" id="example-js" fetchpriority="silly"></script>

which is… silly 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good point. For async/defer, this is handled in the add_data() function:

if ( 'strategy' === $key ) {
if ( ! empty( $value ) && ! $this->is_delayed_strategy( $value ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: $strategy, 2: $handle */
__( 'Invalid strategy `%1$s` defined for `%2$s` during script registration.' ),
$value,
$handle
),
'6.3.0'
);
return false;
} elseif ( ! $this->registered[ $handle ]->src && $this->is_delayed_strategy( $value ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: $strategy, 2: $handle */
__( 'Cannot supply a strategy `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ),
$value,
$handle
),
'6.3.0'
);
return false;
}
}

This would make sense to include there as well for fetchpriority.

}
return $registered;
}

Expand Down Expand Up @@ -339,6 +344,7 @@ function wp_deregister_script( $handle ) {
*
* @since 2.1.0
* @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array.
* @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array.
*
* @param string $handle Name of the script. Should be unique.
* @param string $src Full URL of the script, or path of the script relative to the WordPress root directory.
Expand All @@ -352,8 +358,9 @@ function wp_deregister_script( $handle ) {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'.
* }
*/
function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) {
Expand All @@ -378,6 +385,9 @@ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $
if ( ! empty( $args['strategy'] ) ) {
$wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] );
}
if ( ! empty( $args['fetchpriority'] ) ) {
$wp_scripts->add_data( $_handle[0], 'fetchpriority', $args['fetchpriority'] );
}
}

$wp_scripts->enqueue( $handle );
Expand Down
5 changes: 4 additions & 1 deletion src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,10 @@ function wp_default_scripts( $scripts ) {
did_action( 'init' ) && $scripts->localize( 'wp-plupload', 'pluploadL10n', $uploader_l10n );

$scripts->add( 'comment-reply', "/wp-includes/js/comment-reply$suffix.js", array(), false, 1 );
did_action( 'init' ) && $scripts->add_data( 'comment-reply', 'strategy', 'async' );
if ( did_action( 'init' ) ) {
$scripts->add_data( 'comment-reply', 'strategy', 'async' );
$scripts->add_data( 'comment-reply', 'fetchpriority', 'low' ); // In Chrome this is automatically low due to the async strategy, but in Firefox and Safari the priority is normal/medium.
}

$scripts->add( 'json2', "/wp-includes/js/json2$suffix.js", array(), '2015-05-03' );
did_action( 'init' ) && $scripts->add_data( 'json2', 'conditional', 'lt IE 8' );
Expand Down
Loading
Loading