Skip to content

Introduce fetchpriority for Scripts and Script Modules #8815

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 13 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
13 changes: 12 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;

$args = array();
if (
( isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity'] ) ||
( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] )
) {
// TODO: Add ability for the fetchpriority to be specified in block.json for the viewScriptModule. In wp_default_script_modules() the fetchpriority defaults to low since server-side rendering is employed for core blocks, but there are no guarantees that this is the case for non-core blocks. That said, viewScriptModule entails Interactivity API, following the pattern from core it _should_ be SSR'ed and that is why this is the default for when the block supports interactivity.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe that property in block.json should become an object instead of a string. In the future we might need other properties in there too.

cc @gziolo

Copy link
Member Author

Choose a reason for hiding this comment

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

Absolutely! See below 😄 #8815 (comment)

$args['fetchpriority'] = 'low';
}

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

return $module_id;
Expand Down Expand Up @@ -243,6 +253,7 @@ function register_block_script_handle( $metadata, $field_name, $index = 0 ) {
$script_args = array();
if ( 'viewScript' === $field_name && $script_uri ) {
$script_args['strategy'] = 'defer';
// TODO: There needs to be a way to specify that a script defined in a module is safe to use fetchpriority=low. Perhaps the viewScript should not only allow a handle string or a `file:./foo.js` string, but allow an an array of params like { "href": "./foo.js", "fetchpriority": "low" }. This would allow for more metadata to be supplied, like the script loading strategy. Related: <https://core.trac.wordpress.org/ticket/56408> and <https://core.trac.wordpress.org/ticket/54018>.
Copy link
Member

Choose a reason for hiding this comment

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

Oh, yes, exactly what I meant above 😅

}

$result = wp_register_script(
Expand Down
115 changes: 98 additions & 17 deletions src/wp-includes/class-wp-script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@
* Core class used to register script modules.
*
* @since 6.5.0
*
* @phpstan-type ScriptModule array{
* src: string,
* version: string|false|null,
* enqueue: bool,
* dependencies: array<array{ id: string, import: 'dynamic'|'static' }>,
* fetchpriority: 'auto'|'low'|'high',
* }
Comment on lines +16 to +22
Copy link
Member Author

Choose a reason for hiding this comment

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

I added these @phpstan annotations for myself as a safeguard while developing this patch. If others don't want PHPStan annotations in core, I'll remove prior to commit.

*/
class WP_Script_Modules {
/**
* Holds the registered script modules, keyed by script module identifier.
*
* @since 6.5.0
* @var array[]
* @phpstan-var array<string, ScriptModule>
*/
private $registered = array();

Expand Down Expand Up @@ -46,6 +55,7 @@ class WP_Script_Modules {
* identifier has already been registered.
*
* @since 6.5.0
* @since n.e.x.t 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 +81,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,22 +110,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__,
/* translators: %s: Invalid fetchpriority. */
sprintf( __( 'Invalid fetchpriority: %s' ), $args['fetchpriority'] ),
'n.e.x.t'
);
}
}

$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 n.e.x.t
*
* @param 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 n.e.x.t
*
* @param string $id Script module identifier.
* @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
*/
public function set_fetchpriority( string $id, string $priority ) {
if ( ! isset( $this->registered[ $id ] ) ) {
return;
}

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

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

/**
* Marks the script module to be enqueued in the page.
*
* If a src is provided and the script module has not been registered yet, it
* will be registered.
*
* @since 6.5.0
* @since n.e.x.t 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 +205,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, $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 +282,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 +307,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 +355,9 @@ private function get_import_map(): array {
*
* @since 6.5.0
*
* @return array[] Script modules marked for enqueue, keyed by script module identifier.
* @phpstan-return array<string, ScriptModule>
*
* @return array<string, array> Script modules marked for enqueue, keyed by script module identifier.
*/
private function get_marked_for_enqueue(): array {
$enqueued = array();
Expand All @@ -300,6 +379,8 @@ private function get_marked_for_enqueue(): array {
*
* @since 6.5.0
*
* @phpstan-return array<string, ScriptModule>
*
* @param string[] $ids The identifiers of the script modules for which to gather dependencies.
* @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
* Default is both.
Expand Down
3 changes: 3 additions & 0 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'] ) {
$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
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 n.e.x.t 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'] );
}
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 n.e.x.t 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 @@ -1053,7 +1053,10 @@ function wp_default_scripts( $scripts ) {
did_action( 'init' ) && $scripts->localize( 'swfupload-handlers', 'swfuploadL10n', $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
28 changes: 23 additions & 5 deletions src/wp-includes/script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function wp_script_modules(): WP_Script_Modules {
* identifier has already been registered.
*
* @since 6.5.0
* @since n.e.x.t 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 @@ -60,9 +61,14 @@ function wp_script_modules(): 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.
* }
*/
function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) {
wp_script_modules()->register( $id, $src, $deps, $version );
function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
wp_script_modules()->register( $id, $src, $deps, $version, $args );
}

/**
Expand All @@ -72,6 +78,7 @@ function wp_register_script_module( string $id, string $src, array $deps = array
* will be registered.
*
* @since 6.5.0
* @since n.e.x.t 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 @@ -97,9 +104,14 @@ function wp_register_script_module( string $id, string $src, array $deps = array
* 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.
* }
*/
function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) {
wp_script_modules()->enqueue( $id, $src, $deps, $version );
function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
wp_script_modules()->enqueue( $id, $src, $deps, $version, $args );
}

/**
Expand Down Expand Up @@ -169,7 +181,13 @@ function wp_default_script_modules() {
break;
}

// The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules should be loaded with low fetch priority since they should not be needed in the critical rendering path.
$args = array();
if ( str_starts_with( $script_module_id, '@wordpress/interactivity' ) || str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
$args['fetchpriority'] = 'low';
}

$path = includes_url( "js/dist/script-modules/{$file_name}" );
wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] );
wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'], $args );
}
}
Loading
Loading