Changeset 3229867
- Timestamp:
- 01/27/2025 06:22:54 PM (12 months ago)
- Location:
- speculation-rules
- Files:
-
- 12 edited
- 1 copied
-
tags/1.4.0 (copied) (copied from speculation-rules/trunk)
-
tags/1.4.0/class-plsr-url-pattern-prefixer.php (modified) (2 diffs)
-
tags/1.4.0/helper.php (modified) (4 diffs)
-
tags/1.4.0/hooks.php (modified) (2 diffs)
-
tags/1.4.0/load.php (modified) (3 diffs)
-
tags/1.4.0/readme.txt (modified) (5 diffs)
-
tags/1.4.0/settings.php (modified) (12 diffs)
-
trunk/class-plsr-url-pattern-prefixer.php (modified) (2 diffs)
-
trunk/helper.php (modified) (4 diffs)
-
trunk/hooks.php (modified) (2 diffs)
-
trunk/load.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/settings.php (modified) (12 diffs)
Legend:
- Unmodified
- Added
- Removed
-
speculation-rules/tags/1.4.0/class-plsr-url-pattern-prefixer.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 36 37 */ 37 38 public function __construct( array $contexts = array() ) { 38 if ( $contexts) {39 if ( count( $contexts ) > 0 ) { 39 40 $this->contexts = array_map( 40 41 static function ( string $str ): string { -
speculation-rules/tags/1.4.0/helper.php
r3098880 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 20 21 * @since 1.0.0 21 22 * 22 * @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.23 * @return non-empty-array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type. 23 24 */ 24 25 function plsr_get_speculation_rules(): array { 25 $option = get_option( 'plsr_speculation_rules' ); 26 27 /* 28 * This logic is only relevant for edge-cases where the setting may not be registered, 29 * a.k.a. defensive coding. 30 */ 31 if ( ! $option || ! is_array( $option ) ) { 32 $option = plsr_get_setting_default(); 33 } else { 34 $option = array_merge( plsr_get_setting_default(), $option ); 35 } 36 37 $mode = (string) $option['mode']; 26 $option = plsr_get_stored_setting_value(); 27 $mode = $option['mode']; 38 28 $eagerness = $option['eagerness']; 39 29 … … 41 31 42 32 $base_href_exclude_paths = array( 43 $prefixer->prefix_path_pattern( '/wp- login.php', 'site' ),33 $prefixer->prefix_path_pattern( '/wp-*.php', 'site' ), 44 34 $prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ), 45 $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ),46 35 $prefixer->prefix_path_pattern( '/*', 'uploads' ), 47 36 $prefixer->prefix_path_pattern( '/*', 'content' ), … … 51 40 ); 52 41 42 /* 43 * If pretty permalinks are enabled, exclude any URLs with query parameters. 44 * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter. 45 */ 46 if ( (bool) get_option( 'permalink_structure' ) ) { 47 $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' ); 48 } else { 49 $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ); 50 } 51 53 52 /** 54 53 * Filters the paths for which speculative prerendering should be disabled. 55 54 * 56 55 * All paths should start in a forward slash, relative to the root document. The `*` can be used as a wildcard. 57 * By default, the array includes `/wp-login.php` and `/wp-admin/*`.58 56 * 59 57 * If the WordPress site is in a subdirectory, the exclude paths will automatically be prefixed as necessary. -
speculation-rules/tags/1.4.0/hooks.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 20 21 */ 21 22 function plsr_print_speculation_rules(): void { 22 $rules = plsr_get_speculation_rules();23 if ( empty( $rules) ) {23 // Skip speculative loading for logged-in users. 24 if ( is_user_logged_in() ) { 24 25 return; 25 26 } 26 27 27 // This workaround is needed for WP 6.4. See <https://core.trac.wordpress.org/ticket/60320>. 28 $needs_html5_workaround = ( 29 ! current_theme_supports( 'html5', 'script' ) && 30 version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) && 31 version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.5', '<' ) 32 ); 33 if ( $needs_html5_workaround ) { 34 $backup_wp_theme_features = $GLOBALS['_wp_theme_features']; 35 add_theme_support( 'html5', array( 'script' ) ); 28 // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled. 29 if ( ! (bool) get_option( 'permalink_structure' ) ) { 30 /** 31 * Filters whether speculative loading should be enabled even though the site does not use pretty permalinks. 32 * 33 * Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any 34 * such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are 35 * impossible to recognize. Therefore speculative loading is disabled by default for those sites. 36 * 37 * For site owners of sites without pretty permalinks that are certain their site is not using such a pattern, 38 * this filter can be used to still enable speculative loading at their own risk. 39 * 40 * @since 1.4.0 41 * 42 * @param bool $enabled Whether speculative loading is enabled even without pretty permalinks. 43 */ 44 $enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false ); 45 46 if ( ! $enabled ) { 47 return; 48 } 36 49 } 37 50 38 51 wp_print_inline_script_tag( 39 (string) wp_json_encode( $rules),52 (string) wp_json_encode( plsr_get_speculation_rules() ), 40 53 array( 'type' => 'speculationrules' ) 41 54 ); 42 43 if ( $needs_html5_workaround ) {44 $GLOBALS['_wp_theme_features'] = $backup_wp_theme_features; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited45 }46 55 } 47 56 add_action( 'wp_footer', 'plsr_print_speculation_rules' ); -
speculation-rules/tags/1.4.0/load.php
r3098880 r3229867 3 3 * Plugin Name: Speculative Loading 4 4 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/speculation-rules 5 * Description: Enables browsers to speculatively prerender or prefetch pages when hovering over links.6 * Requires at least: 6. 45 * Description: Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction. 6 * Requires at least: 6.6 7 7 * Requires PHP: 7.2 8 * Version: 1. 3.18 * Version: 1.4.0 9 9 * Author: WordPress Performance Team 10 10 * Author URI: https://make.wordpress.org/performance/ … … 16 16 */ 17 17 18 // Exit if accessed directly.18 // @codeCoverageIgnoreStart 19 19 if ( ! defined( 'ABSPATH' ) ) { 20 exit; 20 exit; // Exit if accessed directly. 21 21 } 22 // @codeCoverageIgnoreEnd 22 23 23 24 ( … … 66 67 )( 67 68 'plsr_pending_plugin_info', 68 '1. 3.1',69 '1.4.0', 69 70 static function ( string $version ): void { 70 71 -
speculation-rules/tags/1.4.0/readme.txt
r3098880 r3229867 1 1 === Speculative Loading === 2 2 3 Contributors: wordpressdotorg 4 Requires at least: 6.4 5 Tested up to: 6.5 6 Requires PHP: 7.2 7 Stable tag: 1.3.1 8 License: GPLv2 or later 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 Tags: performance, javascript, speculation rules, prerender, prefetch 3 Contributors: wordpressdotorg 4 Tested up to: 6.7 5 Stable tag: 1.4.0 6 License: GPLv2 or later 7 License URI: https://www.gnu.org/licenses/gpl-2.0.html 8 Tags: performance, javascript, speculation rules, prerender, prefetch 11 9 12 Enables browsers to speculatively prerender or prefetch pages when hovering over links.10 Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction. 13 11 14 12 == Description == 15 13 16 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered based on user interaction.14 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered. 17 15 18 16 See the [Speculation Rules WICG specification draft](https://wicg.github.io/nav-speculation/speculation-rules.html). 19 17 20 By default, the plugin is configured to prerender WordPress frontend URLs when the user hovers over a relevant link. This can be customized via the "Speculative Loading" section under _Settings > Reading_.18 By default, the plugin is configured to prerender WordPress frontend URLs when the user interacts with a relevant link. This can be customized via the "Speculative Loading" section in the _Settings > Reading_ admin screen. 21 19 22 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the 'no-prerender'CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.20 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the `no-prerender` CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information. 23 21 24 22 = Browser support = 25 23 26 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects but will not benefit from the speculative loading. Note that extensions may disable preloading by default (for example, uBlock Origin does this). 27 28 Other browsers will not see any adverse effects, however the feature will not work for those clients. 24 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects; they will simply not benefit from the speculative loading. Note that certain browser extensions may disable preloading by default. 29 25 30 26 * [Browser support for the Speculation Rules API in general](https://caniuse.com/mdn-html_elements_script_type_speculationrules) 31 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/ blog/chrome-121-beta#speculation_rules_api)27 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/docs/web-platform/prerender-pages) 32 28 33 29 _This plugin was formerly known as Speculation Rules._ … … 51 47 = How can I prevent certain URLs from being prefetched and prerendered? = 52 48 53 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URL generated with `wp_nonce_url()` (or which contain the `_wpnonce` query var) isalso ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.49 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URLs generated with `wp_nonce_url()` (or which contains the `_wpnonce` query var) and `nofollow` links are also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter. 54 50 55 Th is example would ensure that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` would be excluded from prefetching and prerendering.51 The following example ensures that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` are excluded from prefetching and prerendering: 56 52 ` 57 53 <?php 58 59 54 add_filter( 60 55 'plsr_speculation_rules_href_exclude_paths', … … 70 65 For this purpose, the `plsr_speculation_rules_href_exclude_paths` filter receives the current mode (either "prefetch" or "prerender") to provide conditional exclusions. 71 66 72 The following example would ensure that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched.67 The following example ensures that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched: 73 68 ` 74 69 <?php 75 76 70 add_filter( 77 71 'plsr_speculation_rules_href_exclude_paths', … … 89 83 As mentioned above, adding the `no-prerender` CSS class to a link will prevent it from being prerendered (but not prefetched). Additionally, links with `rel=nofollow` will neither be prefetched nor prerendered because some plugins add this to non-idempotent links (e.g. add to cart); such links ideally should rather be buttons which trigger a POST request or at least they should use `wp_nonce_url()`. 90 84 85 = Are there any special considerations for speculative loading behavior? = 86 87 For safety reasons, the entire speculative loading feature is disabled by default for logged-in users and for sites that do not use pretty permalinks. The latter is the case because plugins often use URLs with custom query parameters to let users perform actions, and such URLs should not be speculatively loaded. For sites without pretty permalinks, it is impossible or at least extremely complex to differentiate between which query parameters are Core defaults and which query parameters are custom. 88 89 If you are running this plugin on a site without pretty permalinks and are confident that there are no custom query parameters in use that can cause state changes, you can opt in to enabling speculative loading via the `plsr_enabled_without_pretty_permalinks` filter: 90 91 ` 92 <?php 93 add_filter( 'plsr_enabled_without_pretty_permalinks', '__return_true' ); 94 ` 95 91 96 = How will this impact analytics and personalization? = 92 97 93 98 Prerendering can affect analytics and personalization. 94 99 95 For client-side JavaScript, is recommended to delay these until the p age clicks and some solutions (like Google Analytics) already do this automatically for prerender. See [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.100 For client-side JavaScript, is recommended to delay these until the prerender is activated (for example by clicking on the link). Some solutions (like Google Analytics) already do this automatically, see [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here. 96 101 97 Speculating on hover (moderate) increases the chance the page will be loaded, over preloading without this signal, and thus reduces the risk here. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which further reduces the risk hereand is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.102 Speculating with the default `moderate` eagerness decreases the risk that the prerendered page will not be visited by the user and therefore will avoid any side effects of loading such a link in advance. In contrast, `eager` speculation increases the risk that prerendered pages may not be loaded. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which reduces the risk even further and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain. 98 103 99 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) may require a new page load, and hence a new prerender anyway, which will take these into account. But it definitely is something to be aware of and test!104 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) often require a new page load, and hence a new prerender, which will then take these into account. But it definitely is something to be aware of and test! Prerendered pages can be canceled by removing the speculation rules `<script>` element from the page using standard JavaScript DOM APIs should this be needed when state changes without a new page load. 100 105 101 106 = Where can I submit my plugin feedback? = … … 114 119 115 120 == Changelog == 121 122 = 1.4.0 = 123 124 **Enhancements** 125 126 * Implement speculative loading considerations for safer behavior. ([1784](https://github.com/WordPress/performance/pull/1784)) 116 127 117 128 = 1.3.1 = -
speculation-rules/tags/1.4.0/settings.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 12 } 11 exit; // Exit if accessed directly. 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 17 18 * @since 1.0.0 18 19 * 19 * @return array <string, string>Associative array of `$mode => $label` pairs.20 * @return array{ prefetch: string, prerender: string } Associative array of `$mode => $label` pairs. 20 21 */ 21 22 function plsr_get_mode_labels(): array { … … 31 32 * @since 1.0.0 32 33 * 33 * @return array <string, string>Associative array of `$eagerness => $label` pairs.34 * @return array{ conservative: string, moderate: string, eager: string } Associative array of `$eagerness => $label` pairs. 34 35 */ 35 36 function plsr_get_eagerness_labels(): array { … … 46 47 * @since 1.0.0 47 48 * 48 * @return array <string, string>{49 * @return array{ mode: 'prerender', eagerness: 'moderate' } { 49 50 * Default setting value. 50 51 * … … 61 62 62 63 /** 63 * Sanitizes the setting for Speculative Loading configuration. 64 * 65 * @since 1.0.0 66 * 67 * @param mixed $input Setting to sanitize. 68 * @return array<string, string> { 69 * Sanitized setting. 64 * Returns the stored setting value for Speculative Loading configuration. 65 * 66 * @since 1.4.0 67 * 68 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } { 69 * Stored setting value. 70 70 * 71 71 * @type string $mode Mode. … … 73 73 * } 74 74 */ 75 function plsr_get_stored_setting_value(): array { 76 return plsr_sanitize_setting( get_option( 'plsr_speculation_rules' ) ); 77 } 78 79 /** 80 * Sanitizes the setting for Speculative Loading configuration. 81 * 82 * @since 1.0.0 83 * @todo Consider whether the JSON schema for the setting could be reused here. 84 * 85 * @param mixed $input Setting to sanitize. 86 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } { 87 * Sanitized setting. 88 * 89 * @type string $mode Mode. 90 * @type string $eagerness Eagerness. 91 * } 92 */ 75 93 function plsr_sanitize_setting( $input ): array { 76 94 $default_value = plsr_get_setting_default(); … … 80 98 } 81 99 82 $mode_labels = plsr_get_mode_labels();83 $eagerness_labels = plsr_get_eagerness_labels();84 85 100 // Ensure only valid keys are present. 86 $value = array_intersect_key( $input, $default_value );87 88 // Set any missing or invalid values to their defaults.89 if ( ! i sset( $value['mode'] ) || ! isset( $mode_labels[ $value['mode'] ]) ) {101 $value = array_intersect_key( array_merge( $default_value, $input ), $default_value ); 102 103 // Constrain values to what is allowed. 104 if ( ! in_array( $value['mode'], array_keys( plsr_get_mode_labels() ), true ) ) { 90 105 $value['mode'] = $default_value['mode']; 91 106 } 92 if ( ! i sset( $value['eagerness'] ) || ! isset( $eagerness_labels[ $value['eagerness'] ]) ) {107 if ( ! in_array( $value['eagerness'], array_keys( plsr_get_eagerness_labels() ), true ) ) { 93 108 $value['eagerness'] = $default_value['eagerness']; 94 109 } … … 114 129 'show_in_rest' => array( 115 130 'schema' => array( 116 'properties' => array( 131 'type' => 'object', 132 'properties' => array( 117 133 'mode' => array( 118 134 'description' => __( 'Whether to prefetch or prerender URLs.', 'speculation-rules' ), … … 126 142 ), 127 143 ), 144 'additionalProperties' => false, 128 145 ), 129 146 ), … … 189 206 * @access private 190 207 * 191 * @param array <string, string>$args {208 * @param array{ field: 'mode'|'eagerness', title: non-empty-string, description: non-empty-string } $args { 192 209 * Associative array of arguments. 193 210 * … … 198 215 */ 199 216 function plsr_render_settings_field( array $args ): void { 200 if ( empty( $args['field'] ) || empty( $args['title'] ) ) { // Invalid. 201 return; 202 } 203 204 $option = get_option( 'plsr_speculation_rules' ); 205 if ( ! isset( $option[ $args['field'] ] ) ) { // Invalid. 206 return; 207 } 208 209 $value = $option[ $args['field'] ]; 210 $callback = "plsr_get_{$args['field']}_labels"; 211 if ( ! is_callable( $callback ) ) { 212 return; 213 } 214 $choices = call_user_func( $callback ); 215 217 $option = plsr_get_stored_setting_value(); 218 219 switch ( $args['field'] ) { 220 case 'mode': 221 $choices = plsr_get_mode_labels(); 222 break; 223 case 'eagerness': 224 $choices = plsr_get_eagerness_labels(); 225 break; 226 default: 227 return; // Invalid (and this case should never occur). 228 } 229 230 $value = $option[ $args['field'] ]; 216 231 ?> 217 232 <fieldset> 218 233 <legend class="screen-reader-text"><?php echo esc_html( $args['title'] ); ?></legend> 219 <?php 220 foreach ( $choices as $slug => $label ) { 221 ?> 234 <?php foreach ( $choices as $slug => $label ) : ?> 222 235 <p> 223 236 <label> … … 231 244 </label> 232 245 </p> 233 <?php 234 } 235 236 if ( ! empty( $args['description'] ) ) { 237 ?> 238 <p class="description" style="max-width: 800px;"> 239 <?php echo esc_html( $args['description'] ); ?> 240 </p> 241 <?php 242 } 243 ?> 246 <?php endforeach; ?> 247 248 <p class="description" style="max-width: 800px;"> 249 <?php echo esc_html( $args['description'] ); ?> 250 </p> 244 251 </fieldset> 245 252 <?php -
speculation-rules/trunk/class-plsr-url-pattern-prefixer.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 36 37 */ 37 38 public function __construct( array $contexts = array() ) { 38 if ( $contexts) {39 if ( count( $contexts ) > 0 ) { 39 40 $this->contexts = array_map( 40 41 static function ( string $str ): string { -
speculation-rules/trunk/helper.php
r3098880 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 20 21 * @since 1.0.0 21 22 * 22 * @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.23 * @return non-empty-array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type. 23 24 */ 24 25 function plsr_get_speculation_rules(): array { 25 $option = get_option( 'plsr_speculation_rules' ); 26 27 /* 28 * This logic is only relevant for edge-cases where the setting may not be registered, 29 * a.k.a. defensive coding. 30 */ 31 if ( ! $option || ! is_array( $option ) ) { 32 $option = plsr_get_setting_default(); 33 } else { 34 $option = array_merge( plsr_get_setting_default(), $option ); 35 } 36 37 $mode = (string) $option['mode']; 26 $option = plsr_get_stored_setting_value(); 27 $mode = $option['mode']; 38 28 $eagerness = $option['eagerness']; 39 29 … … 41 31 42 32 $base_href_exclude_paths = array( 43 $prefixer->prefix_path_pattern( '/wp- login.php', 'site' ),33 $prefixer->prefix_path_pattern( '/wp-*.php', 'site' ), 44 34 $prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ), 45 $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ),46 35 $prefixer->prefix_path_pattern( '/*', 'uploads' ), 47 36 $prefixer->prefix_path_pattern( '/*', 'content' ), … … 51 40 ); 52 41 42 /* 43 * If pretty permalinks are enabled, exclude any URLs with query parameters. 44 * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter. 45 */ 46 if ( (bool) get_option( 'permalink_structure' ) ) { 47 $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' ); 48 } else { 49 $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ); 50 } 51 53 52 /** 54 53 * Filters the paths for which speculative prerendering should be disabled. 55 54 * 56 55 * All paths should start in a forward slash, relative to the root document. The `*` can be used as a wildcard. 57 * By default, the array includes `/wp-login.php` and `/wp-admin/*`.58 56 * 59 57 * If the WordPress site is in a subdirectory, the exclude paths will automatically be prefixed as necessary. -
speculation-rules/trunk/hooks.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 11 exit; // Exit if accessed directly. 12 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 20 21 */ 21 22 function plsr_print_speculation_rules(): void { 22 $rules = plsr_get_speculation_rules();23 if ( empty( $rules) ) {23 // Skip speculative loading for logged-in users. 24 if ( is_user_logged_in() ) { 24 25 return; 25 26 } 26 27 27 // This workaround is needed for WP 6.4. See <https://core.trac.wordpress.org/ticket/60320>. 28 $needs_html5_workaround = ( 29 ! current_theme_supports( 'html5', 'script' ) && 30 version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) && 31 version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.5', '<' ) 32 ); 33 if ( $needs_html5_workaround ) { 34 $backup_wp_theme_features = $GLOBALS['_wp_theme_features']; 35 add_theme_support( 'html5', array( 'script' ) ); 28 // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled. 29 if ( ! (bool) get_option( 'permalink_structure' ) ) { 30 /** 31 * Filters whether speculative loading should be enabled even though the site does not use pretty permalinks. 32 * 33 * Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any 34 * such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are 35 * impossible to recognize. Therefore speculative loading is disabled by default for those sites. 36 * 37 * For site owners of sites without pretty permalinks that are certain their site is not using such a pattern, 38 * this filter can be used to still enable speculative loading at their own risk. 39 * 40 * @since 1.4.0 41 * 42 * @param bool $enabled Whether speculative loading is enabled even without pretty permalinks. 43 */ 44 $enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false ); 45 46 if ( ! $enabled ) { 47 return; 48 } 36 49 } 37 50 38 51 wp_print_inline_script_tag( 39 (string) wp_json_encode( $rules),52 (string) wp_json_encode( plsr_get_speculation_rules() ), 40 53 array( 'type' => 'speculationrules' ) 41 54 ); 42 43 if ( $needs_html5_workaround ) {44 $GLOBALS['_wp_theme_features'] = $backup_wp_theme_features; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited45 }46 55 } 47 56 add_action( 'wp_footer', 'plsr_print_speculation_rules' ); -
speculation-rules/trunk/load.php
r3098880 r3229867 3 3 * Plugin Name: Speculative Loading 4 4 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/speculation-rules 5 * Description: Enables browsers to speculatively prerender or prefetch pages when hovering over links.6 * Requires at least: 6. 45 * Description: Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction. 6 * Requires at least: 6.6 7 7 * Requires PHP: 7.2 8 * Version: 1. 3.18 * Version: 1.4.0 9 9 * Author: WordPress Performance Team 10 10 * Author URI: https://make.wordpress.org/performance/ … … 16 16 */ 17 17 18 // Exit if accessed directly.18 // @codeCoverageIgnoreStart 19 19 if ( ! defined( 'ABSPATH' ) ) { 20 exit; 20 exit; // Exit if accessed directly. 21 21 } 22 // @codeCoverageIgnoreEnd 22 23 23 24 ( … … 66 67 )( 67 68 'plsr_pending_plugin_info', 68 '1. 3.1',69 '1.4.0', 69 70 static function ( string $version ): void { 70 71 -
speculation-rules/trunk/readme.txt
r3098880 r3229867 1 1 === Speculative Loading === 2 2 3 Contributors: wordpressdotorg 4 Requires at least: 6.4 5 Tested up to: 6.5 6 Requires PHP: 7.2 7 Stable tag: 1.3.1 8 License: GPLv2 or later 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 Tags: performance, javascript, speculation rules, prerender, prefetch 3 Contributors: wordpressdotorg 4 Tested up to: 6.7 5 Stable tag: 1.4.0 6 License: GPLv2 or later 7 License URI: https://www.gnu.org/licenses/gpl-2.0.html 8 Tags: performance, javascript, speculation rules, prerender, prefetch 11 9 12 Enables browsers to speculatively prerender or prefetch pages when hovering over links.10 Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction. 13 11 14 12 == Description == 15 13 16 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered based on user interaction.14 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered. 17 15 18 16 See the [Speculation Rules WICG specification draft](https://wicg.github.io/nav-speculation/speculation-rules.html). 19 17 20 By default, the plugin is configured to prerender WordPress frontend URLs when the user hovers over a relevant link. This can be customized via the "Speculative Loading" section under _Settings > Reading_.18 By default, the plugin is configured to prerender WordPress frontend URLs when the user interacts with a relevant link. This can be customized via the "Speculative Loading" section in the _Settings > Reading_ admin screen. 21 19 22 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the 'no-prerender'CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.20 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the `no-prerender` CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information. 23 21 24 22 = Browser support = 25 23 26 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects but will not benefit from the speculative loading. Note that extensions may disable preloading by default (for example, uBlock Origin does this). 27 28 Other browsers will not see any adverse effects, however the feature will not work for those clients. 24 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects; they will simply not benefit from the speculative loading. Note that certain browser extensions may disable preloading by default. 29 25 30 26 * [Browser support for the Speculation Rules API in general](https://caniuse.com/mdn-html_elements_script_type_speculationrules) 31 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/ blog/chrome-121-beta#speculation_rules_api)27 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/docs/web-platform/prerender-pages) 32 28 33 29 _This plugin was formerly known as Speculation Rules._ … … 51 47 = How can I prevent certain URLs from being prefetched and prerendered? = 52 48 53 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URL generated with `wp_nonce_url()` (or which contain the `_wpnonce` query var) isalso ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.49 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URLs generated with `wp_nonce_url()` (or which contains the `_wpnonce` query var) and `nofollow` links are also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter. 54 50 55 Th is example would ensure that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` would be excluded from prefetching and prerendering.51 The following example ensures that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` are excluded from prefetching and prerendering: 56 52 ` 57 53 <?php 58 59 54 add_filter( 60 55 'plsr_speculation_rules_href_exclude_paths', … … 70 65 For this purpose, the `plsr_speculation_rules_href_exclude_paths` filter receives the current mode (either "prefetch" or "prerender") to provide conditional exclusions. 71 66 72 The following example would ensure that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched.67 The following example ensures that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched: 73 68 ` 74 69 <?php 75 76 70 add_filter( 77 71 'plsr_speculation_rules_href_exclude_paths', … … 89 83 As mentioned above, adding the `no-prerender` CSS class to a link will prevent it from being prerendered (but not prefetched). Additionally, links with `rel=nofollow` will neither be prefetched nor prerendered because some plugins add this to non-idempotent links (e.g. add to cart); such links ideally should rather be buttons which trigger a POST request or at least they should use `wp_nonce_url()`. 90 84 85 = Are there any special considerations for speculative loading behavior? = 86 87 For safety reasons, the entire speculative loading feature is disabled by default for logged-in users and for sites that do not use pretty permalinks. The latter is the case because plugins often use URLs with custom query parameters to let users perform actions, and such URLs should not be speculatively loaded. For sites without pretty permalinks, it is impossible or at least extremely complex to differentiate between which query parameters are Core defaults and which query parameters are custom. 88 89 If you are running this plugin on a site without pretty permalinks and are confident that there are no custom query parameters in use that can cause state changes, you can opt in to enabling speculative loading via the `plsr_enabled_without_pretty_permalinks` filter: 90 91 ` 92 <?php 93 add_filter( 'plsr_enabled_without_pretty_permalinks', '__return_true' ); 94 ` 95 91 96 = How will this impact analytics and personalization? = 92 97 93 98 Prerendering can affect analytics and personalization. 94 99 95 For client-side JavaScript, is recommended to delay these until the p age clicks and some solutions (like Google Analytics) already do this automatically for prerender. See [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.100 For client-side JavaScript, is recommended to delay these until the prerender is activated (for example by clicking on the link). Some solutions (like Google Analytics) already do this automatically, see [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here. 96 101 97 Speculating on hover (moderate) increases the chance the page will be loaded, over preloading without this signal, and thus reduces the risk here. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which further reduces the risk hereand is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.102 Speculating with the default `moderate` eagerness decreases the risk that the prerendered page will not be visited by the user and therefore will avoid any side effects of loading such a link in advance. In contrast, `eager` speculation increases the risk that prerendered pages may not be loaded. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which reduces the risk even further and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain. 98 103 99 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) may require a new page load, and hence a new prerender anyway, which will take these into account. But it definitely is something to be aware of and test!104 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) often require a new page load, and hence a new prerender, which will then take these into account. But it definitely is something to be aware of and test! Prerendered pages can be canceled by removing the speculation rules `<script>` element from the page using standard JavaScript DOM APIs should this be needed when state changes without a new page load. 100 105 101 106 = Where can I submit my plugin feedback? = … … 114 119 115 120 == Changelog == 121 122 = 1.4.0 = 123 124 **Enhancements** 125 126 * Implement speculative loading considerations for safer behavior. ([1784](https://github.com/WordPress/performance/pull/1784)) 116 127 117 128 = 1.3.1 = -
speculation-rules/trunk/settings.php
r3089572 r3229867 7 7 */ 8 8 9 // Exit if accessed directly.9 // @codeCoverageIgnoreStart 10 10 if ( ! defined( 'ABSPATH' ) ) { 11 exit; 12 } 11 exit; // Exit if accessed directly. 12 } 13 // @codeCoverageIgnoreEnd 13 14 14 15 /** … … 17 18 * @since 1.0.0 18 19 * 19 * @return array <string, string>Associative array of `$mode => $label` pairs.20 * @return array{ prefetch: string, prerender: string } Associative array of `$mode => $label` pairs. 20 21 */ 21 22 function plsr_get_mode_labels(): array { … … 31 32 * @since 1.0.0 32 33 * 33 * @return array <string, string>Associative array of `$eagerness => $label` pairs.34 * @return array{ conservative: string, moderate: string, eager: string } Associative array of `$eagerness => $label` pairs. 34 35 */ 35 36 function plsr_get_eagerness_labels(): array { … … 46 47 * @since 1.0.0 47 48 * 48 * @return array <string, string>{49 * @return array{ mode: 'prerender', eagerness: 'moderate' } { 49 50 * Default setting value. 50 51 * … … 61 62 62 63 /** 63 * Sanitizes the setting for Speculative Loading configuration. 64 * 65 * @since 1.0.0 66 * 67 * @param mixed $input Setting to sanitize. 68 * @return array<string, string> { 69 * Sanitized setting. 64 * Returns the stored setting value for Speculative Loading configuration. 65 * 66 * @since 1.4.0 67 * 68 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } { 69 * Stored setting value. 70 70 * 71 71 * @type string $mode Mode. … … 73 73 * } 74 74 */ 75 function plsr_get_stored_setting_value(): array { 76 return plsr_sanitize_setting( get_option( 'plsr_speculation_rules' ) ); 77 } 78 79 /** 80 * Sanitizes the setting for Speculative Loading configuration. 81 * 82 * @since 1.0.0 83 * @todo Consider whether the JSON schema for the setting could be reused here. 84 * 85 * @param mixed $input Setting to sanitize. 86 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } { 87 * Sanitized setting. 88 * 89 * @type string $mode Mode. 90 * @type string $eagerness Eagerness. 91 * } 92 */ 75 93 function plsr_sanitize_setting( $input ): array { 76 94 $default_value = plsr_get_setting_default(); … … 80 98 } 81 99 82 $mode_labels = plsr_get_mode_labels();83 $eagerness_labels = plsr_get_eagerness_labels();84 85 100 // Ensure only valid keys are present. 86 $value = array_intersect_key( $input, $default_value );87 88 // Set any missing or invalid values to their defaults.89 if ( ! i sset( $value['mode'] ) || ! isset( $mode_labels[ $value['mode'] ]) ) {101 $value = array_intersect_key( array_merge( $default_value, $input ), $default_value ); 102 103 // Constrain values to what is allowed. 104 if ( ! in_array( $value['mode'], array_keys( plsr_get_mode_labels() ), true ) ) { 90 105 $value['mode'] = $default_value['mode']; 91 106 } 92 if ( ! i sset( $value['eagerness'] ) || ! isset( $eagerness_labels[ $value['eagerness'] ]) ) {107 if ( ! in_array( $value['eagerness'], array_keys( plsr_get_eagerness_labels() ), true ) ) { 93 108 $value['eagerness'] = $default_value['eagerness']; 94 109 } … … 114 129 'show_in_rest' => array( 115 130 'schema' => array( 116 'properties' => array( 131 'type' => 'object', 132 'properties' => array( 117 133 'mode' => array( 118 134 'description' => __( 'Whether to prefetch or prerender URLs.', 'speculation-rules' ), … … 126 142 ), 127 143 ), 144 'additionalProperties' => false, 128 145 ), 129 146 ), … … 189 206 * @access private 190 207 * 191 * @param array <string, string>$args {208 * @param array{ field: 'mode'|'eagerness', title: non-empty-string, description: non-empty-string } $args { 192 209 * Associative array of arguments. 193 210 * … … 198 215 */ 199 216 function plsr_render_settings_field( array $args ): void { 200 if ( empty( $args['field'] ) || empty( $args['title'] ) ) { // Invalid. 201 return; 202 } 203 204 $option = get_option( 'plsr_speculation_rules' ); 205 if ( ! isset( $option[ $args['field'] ] ) ) { // Invalid. 206 return; 207 } 208 209 $value = $option[ $args['field'] ]; 210 $callback = "plsr_get_{$args['field']}_labels"; 211 if ( ! is_callable( $callback ) ) { 212 return; 213 } 214 $choices = call_user_func( $callback ); 215 217 $option = plsr_get_stored_setting_value(); 218 219 switch ( $args['field'] ) { 220 case 'mode': 221 $choices = plsr_get_mode_labels(); 222 break; 223 case 'eagerness': 224 $choices = plsr_get_eagerness_labels(); 225 break; 226 default: 227 return; // Invalid (and this case should never occur). 228 } 229 230 $value = $option[ $args['field'] ]; 216 231 ?> 217 232 <fieldset> 218 233 <legend class="screen-reader-text"><?php echo esc_html( $args['title'] ); ?></legend> 219 <?php 220 foreach ( $choices as $slug => $label ) { 221 ?> 234 <?php foreach ( $choices as $slug => $label ) : ?> 222 235 <p> 223 236 <label> … … 231 244 </label> 232 245 </p> 233 <?php 234 } 235 236 if ( ! empty( $args['description'] ) ) { 237 ?> 238 <p class="description" style="max-width: 800px;"> 239 <?php echo esc_html( $args['description'] ); ?> 240 </p> 241 <?php 242 } 243 ?> 246 <?php endforeach; ?> 247 248 <p class="description" style="max-width: 800px;"> 249 <?php echo esc_html( $args['description'] ); ?> 250 </p> 244 251 </fieldset> 245 252 <?php
Note: See TracChangeset
for help on using the changeset viewer.