Skip to content

Commit

Permalink
Use defer loading strategy for frontend view scripts (#52536)
Browse files Browse the repository at this point in the history
* Load interactivity API scripts with defer loading strategy

* Load comment-reply script with defer loading strategy

* Load search view script with defer loading strategy

* Defer the view scripts for the Navigation and File blocks

* Use DCA event instead of window load event for Navigation viewScripts

* Optimize submenu-on-click view script

* Fully leverage event delegation to allow async loading strategy.
* Keep track of whether a submenu is open to short-circuit event handlers.
* Use passive event listeners.

* Optimize modal view script

* Fully leverage event delegation to allow async loading strategy.
* Remove event listener for anchor clicks in modal when modal is closed.
* Use passive event listeners.

* Utilize asset file for wp-block--search-view

* Conditionally enqueue navigation view scripts only if needed

* Update Navigation block to remove/add view scripts in the same way the File block does

* Update Search block to include view script in same way as File block and Navigation block

* Refactor Search block view script

* Adopt asynchronous loading strategy.
* Use event delegation.
* Collapse expanded blocks when tabbing out of expanded Search block.
* Only attach keydown/keyup event handlers while Search block is expanded.
* Ensure search button's aria-label is translated.
* Ensure Search button's type is restored to 'button' instead of deleting type (since no type is same as 'submit').
* Use passive event listeners.

* Use more DOM properties instead of attributes

* Use more precise reflected terminology

* Remove resolved TODO comment

* Use event delegation to open MicroModal

* Remove excessive type check

* Fix closing submenus when there are multiple Navigation blocks on a page

* Clarify block.json comment

* Add core merge note for comment-reply script strategy

* Use defer strategy instead of async for Navigation and Search blocks

* Defer all block view scripts

* Ensure interactivity scripts remain in footer in WP<6.3

* Remove defer from comment-reply for now

Co-authored-by: Felix Arntz <felixarntz@users.noreply.github.com>

---------

Co-authored-by: Felix Arntz <felixarntz@users.noreply.github.com>
  • Loading branch information
westonruter and felixarntz committed Jul 25, 2023
1 parent eea9e52 commit 6cf6643
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 162 deletions.
34 changes: 34 additions & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,40 @@ function gutenberg_reregister_core_block_types() {

add_action( 'init', 'gutenberg_reregister_core_block_types' );

/**
* Adds the defer loading strategy to all registered blocks.
*
* This function would not be part of core merge. Instead, the register_block_script_handle() function would be patched
* as follows.
*
* ```
* --- a/wp-includes/blocks.php
* +++ b/wp-includes/blocks.php
* @ @ -153,7 +153,8 @ @ function register_block_script_handle( $metadata, $field_name, $index = 0 ) {
* $script_handle,
* $script_uri,
* $script_dependencies,
* - isset( $script_asset['version'] ) ? $script_asset['version'] : false
* + isset( $script_asset['version'] ) ? $script_asset['version'] : false,
* + array( 'strategy' => 'defer' )
* );
* if ( ! $result ) {
* return false;
* ```
*
* @see register_block_script_handle()
*/
function gutenberg_defer_block_view_scripts() {
$block_types = WP_Block_Type_Registry::get_instance()->get_all_registered();
foreach ( $block_types as $block_type ) {
foreach ( $block_type->view_script_handles as $view_script_handle ) {
wp_script_add_data( $view_script_handle, 'strategy', 'defer' );
}
}
}

add_action( 'init', 'gutenberg_defer_block_view_scripts', 100 );

/**
* Deregisters the existing core block type and its assets.
*
Expand Down
24 changes: 18 additions & 6 deletions lib/experimental/interactivity-api/scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,32 @@
*/

/**
* Move interactive scripts to the footer. This is a temporary measure to make
* it work with `wp_store` and it should be replaced with deferred scripts or
* modules.
* Makes sure that interactivity scripts execute after all `wp_store` directives have been printed to the page.
*
* In WordPress 6.3+ this is achieved by printing in the head but marking the scripts with defer. This has the benefit
* of early discovery so the script is loaded by the browser, while at the same time not blocking rendering. In older
* versions of WordPress, this is achieved by loading the scripts in the footer.
*
* @link https://make.wordpress.org/core/2023/07/14/registering-scripts-with-async-and-defer-attributes-in-wordpress-6-3/
*/
function gutenberg_interactivity_move_interactive_scripts_to_the_footer() {
// Move the @wordpress/interactivity package to the footer.
wp_script_add_data( 'wp-interactivity', 'group', 1 );
$supports_defer = version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' );
if ( $supports_defer ) {
// Defer execution of @wordpress/interactivity package but continue loading in head.
wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' );
wp_script_add_data( 'wp-interactivity', 'group', 0 );
} else {
// Move the @wordpress/interactivity package to the footer.
wp_script_add_data( 'wp-interactivity', 'group', 1 );
}

// Move all the view scripts of the interactive blocks to the footer.
$registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
foreach ( array_values( $registered_blocks ) as $block ) {
if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) {
foreach ( $block->view_script_handles as $handle ) {
wp_script_add_data( $handle, 'group', 1 );
// Note that all block view scripts are already made defer by default.
wp_script_add_data( $handle, 'group', $supports_defer ? 0 : 1 );
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/file/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function gutenberg_block_core_file_update_interactive_view_script( $metadata ) {
}

/**
* When the `core/file` block is rendering, check if we need to enqueue the `'wp-block-file-view` script.
* When the `core/file` block is rendering, check if we need to enqueue the `wp-block-file-view` script.
*
* @param array $attributes The block attributes.
* @param string $content The block content.
Expand Down
40 changes: 27 additions & 13 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -671,19 +671,33 @@ function render_block_core_navigation( $attributes, $content, $block ) {
$inner_blocks_html .= '</ul>';
}

// If the script already exists, there is no point in removing it from viewScript.
$should_load_view_script = ( $is_responsive_menu || ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) );
$view_js_file = 'wp-block-navigation-view';
if ( ! wp_script_is( $view_js_file ) ) {
$script_handles = $block->block_type->view_script_handles;

// If the script is not needed, and it is still in the `view_script_handles`, remove it.
if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) );
}
// If the script is needed, but it was previously removed, add it again.
if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file, 'wp-block-navigation-view-2' ) );
$needed_script_map = array(
'wp-block-navigation-view' => ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ),
'wp-block-navigation-view-2' => $is_responsive_menu,
);

$should_load_view_script = false;
if ( gutenberg_should_block_use_interactivity_api( 'core/navigation' ) ) {
// TODO: The script is still loaded even when it isn't needed when the Interactivity API is used.
$should_load_view_script = count( array_filter( $needed_script_map ) ) > 0;
} else {
foreach ( $needed_script_map as $view_script_handle => $is_view_script_needed ) {

// If the script already exists, there is no point in removing it from viewScript.
if ( wp_script_is( $view_script_handle ) ) {
continue;
}

$script_handles = $block->block_type->view_script_handles;

// If the script is not needed, and it is still in the `view_script_handles`, remove it.
if ( ! $is_view_script_needed && in_array( $view_script_handle, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_diff( $script_handles, array( $view_script_handle ) );
}
// If the script is needed, but it was previously removed, add it again.
if ( $is_view_script_needed && ! in_array( $view_script_handle, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_merge( $script_handles, array( $view_script_handle ) );
}
}
}

Expand Down
127 changes: 88 additions & 39 deletions packages/block-library/src/navigation/view-modal.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
/*eslint-env browser*/
/**
* External dependencies
*/
import MicroModal from 'micromodal';

// Responsive navigation toggle.
function navigationToggleModal( modal ) {

/**
* Toggles responsive navigation.
*
* @param {HTMLDivElement} modal
* @param {boolean} isHidden
*/
function navigationToggleModal( modal, isHidden ) {
const dialogContainer = modal.querySelector(
`.wp-block-navigation__responsive-dialog`
);

const isHidden = 'true' === modal.getAttribute( 'aria-hidden' );

modal.classList.toggle( 'has-modal-open', ! isHidden );
dialogContainer.toggleAttribute( 'aria-modal', ! isHidden );

Expand All @@ -23,10 +29,15 @@ function navigationToggleModal( modal ) {
}

// Add a class to indicate the modal is open.
const htmlElement = document.documentElement;
htmlElement.classList.toggle( 'has-modal-open' );
document.documentElement.classList.toggle( 'has-modal-open' );
}

/**
* Checks whether the provided link is an anchor on the current page.
*
* @param {HTMLAnchorElement} node
* @return {boolean} Is anchor.
*/
function isLinkToAnchorOnCurrentPage( node ) {
return (
node.hash &&
Expand All @@ -37,42 +48,80 @@ function isLinkToAnchorOnCurrentPage( node ) {
);
}

window.addEventListener( 'load', () => {
MicroModal.init( {
onShow: navigationToggleModal,
onClose: navigationToggleModal,
openClass: 'is-menu-open',
/**
* Handles effects after opening the modal.
*
* @param {HTMLDivElement} modal
*/
function onShow( modal ) {
navigationToggleModal( modal, false );
modal.addEventListener( 'click', handleAnchorLinkClicksInsideModal, {
passive: true,
} );
}

// Close modal automatically on clicking anchor links inside modal.
const navigationLinks = document.querySelectorAll(
'.wp-block-navigation-item__content'
);
/**
* Handles effects after closing the modal.
*
* @param {HTMLDivElement} modal
*/
function onClose( modal ) {
navigationToggleModal( modal, true );
modal.removeEventListener( 'click', handleAnchorLinkClicksInsideModal, {
passive: true,
} );
}

navigationLinks.forEach( function ( link ) {
// Ignore non-anchor links and anchor links which open on a new tab.
if (
! isLinkToAnchorOnCurrentPage( link ) ||
link.attributes?.target === '_blank'
) {
return;
}
/**
* Handle clicks to anchor links in modal using event delegation by closing modal automatically
*
* @param {UIEvent} event
*/
function handleAnchorLinkClicksInsideModal( event ) {
const link = event.target.closest( '.wp-block-navigation-item__content' );
if ( ! ( link instanceof HTMLAnchorElement ) ) {
return;
}

// Find the specific parent modal for this link
// since .close() won't work without an ID if there are
// multiple navigation menus in a post/page.
const modal = link.closest(
'.wp-block-navigation__responsive-container'
);
const modalId = modal?.getAttribute( 'id' );
// Ignore non-anchor links and anchor links which open on a new tab.
if (
! isLinkToAnchorOnCurrentPage( link ) ||
link.attributes?.target === '_blank'
) {
return;
}

link.addEventListener( 'click', () => {
// check if modal exists and is open before trying to close it
// otherwise Micromodal will toggle the `has-modal-open` class
// on the html tag which prevents scrolling
if ( modalId && modal.classList.contains( 'has-modal-open' ) ) {
MicroModal.close( modalId );
}
} );
} );
} );
// Find the specific parent modal for this link
// since .close() won't work without an ID if there are
// multiple navigation menus in a post/page.
const modal = link.closest( '.wp-block-navigation__responsive-container' );
const modalId = modal?.getAttribute( 'id' );
if ( ! modalId ) {
return;
}

// check if modal exists and is open before trying to close it
// otherwise Micromodal will toggle the `has-modal-open` class
// on the html tag which prevents scrolling
if ( modalId && modal.classList.contains( 'has-modal-open' ) ) {
MicroModal.close( modalId );
}
}

// MicroModal.init() does not support event delegation for the open trigger, so here MicroModal.show() is called manually.
document.addEventListener(
'click',
( event ) => {
/** @type {HTMLElement} */
const target = event.target;

if ( target.dataset.micromodalTrigger ) {
MicroModal.show( target.dataset.micromodalTrigger, {
onShow,
onClose,
openClass: 'is-menu-open',
} );
}
},
{ passive: true }
);
Loading

0 comments on commit 6cf6643

Please sign in to comment.