From c54c3580d7bf5ce552e4b02180117e9fcd11cd15 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Tue, 19 May 2026 12:57:43 +0530 Subject: [PATCH] Emoji: Load wp-emoji-loader as an external script module. Register the emoji detection loader via wp_enqueue_script_module() with low fetch priority and footer placement, and pass settings through the script module data API instead of inlining ~3KB of JavaScript on every page. Props Khokan Sardar. Fixes #64259. --- src/js/_enqueues/lib/emoji-loader.js | 30 ++++++-- src/wp-includes/formatting.php | 92 +++++++++++++++++------- tests/phpunit/tests/formatting/emoji.php | 78 +++++++++++++++----- 3 files changed, 152 insertions(+), 48 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 561d3656a29e1..1a0e471fc8427 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -7,7 +7,7 @@ // Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. /** - * Emoji Settings as exported in PHP via _print_emoji_detection_script(). + * Emoji Settings as exported in PHP via the script module data API. * @typedef WPEmojiSettings * @type {object} * @property {?object} source @@ -16,9 +16,31 @@ * @property {?string} source.wpemoji */ -const settings = /** @type {WPEmojiSettings} */ ( - JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) -); +/** + * Parses the Emoji settings from the script module data element. + * + * @since 7.1.0 + * + * @return {WPEmojiSettings} Emoji settings. + */ +function getEmojiSettings() { + const moduleDataContainer = document.getElementById( + 'wp-script-module-data-wp-emoji-loader' + ); + if ( moduleDataContainer ) { + return JSON.parse( moduleDataContainer.textContent ); + } + + // Back-compat for extensions that may still be printing the legacy element. + const legacySettingsContainer = document.getElementById( 'wp-emoji-settings' ); + if ( legacySettingsContainer ) { + return JSON.parse( legacySettingsContainer.textContent ); + } + + return {}; +} + +const settings = /** @type {WPEmojiSettings} */ ( getEmojiSettings() ); // For compatibility with other scripts that read from this global, in particular wp-includes/js/wp-emoji.js (source file: js/_enqueues/wp/emoji.js). window._wpemojiSettings = settings; diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 498d676f5c20f..cdbfbda7e3301 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5894,34 +5894,63 @@ function wp_enqueue_emoji_styles() { } /** - * Prints the inline Emoji detection script if it is not already printed. + * Enqueues the Emoji detection script module if it is not already enqueued. * * @since 4.2.0 */ function print_emoji_detection_script() { - static $printed = false; + static $enqueued = false; - if ( $printed ) { + if ( $enqueued ) { return; } - $printed = true; + $enqueued = true; - if ( did_action( 'wp_print_footer_scripts' ) ) { - _print_emoji_detection_script(); - } else { - add_action( 'wp_print_footer_scripts', '_print_emoji_detection_script' ); - } + _wp_enqueue_emoji_detection_script(); } /** - * Prints inline Emoji detection script. + * Enqueues the Emoji detection script module. * * @ignore * @since 4.6.0 + * @since 7.1.0 The emoji loader is enqueued as an external script module. * @access private */ -function _print_emoji_detection_script() { +function _wp_enqueue_emoji_detection_script() { + if ( ! has_filter( 'script_module_data_wp-emoji-loader', '_wp_emoji_settings_script_module_data' ) ) { + add_filter( 'script_module_data_wp-emoji-loader', '_wp_emoji_settings_script_module_data' ); + } + + $emoji_loader_script_path = '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js'; + $src = includes_url( $emoji_loader_script_path ); + + /** This filter is documented in wp-includes/class-wp-scripts.php */ + $src = apply_filters( 'script_loader_src', $src, 'wp-emoji-loader' ); + + wp_enqueue_script_module( + 'wp-emoji-loader', + $src, + array(), + false, + array( + 'fetchpriority' => 'low', + 'in_footer' => true, + ) + ); +} + +/** + * Returns the Emoji settings for the script module. + * + * @ignore + * @since 7.1.0 + * @access private + * + * @return array Emoji settings. + */ +function _wp_get_emoji_settings(): array { $settings = array( /** * Filters the URL where emoji png images are hosted. @@ -5976,22 +6005,33 @@ function _print_emoji_detection_script() { ); } - wp_print_inline_script_tag( - wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), - array( - 'id' => 'wp-emoji-settings', - 'type' => 'application/json', - ) - ); + return $settings; +} - $emoji_loader_script_path = '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js'; - wp_print_inline_script_tag( - rtrim( file_get_contents( ABSPATH . WPINC . $emoji_loader_script_path ) ) . "\n" . - '//# sourceURL=' . esc_url_raw( includes_url( $emoji_loader_script_path ) ), - array( - 'type' => 'module', - ) - ); +/** + * Filters the Emoji settings passed to the script module. + * + * @ignore + * @since 7.1.0 + * @access private + * + * @param array $data Script module data. + * @return array Emoji settings. + */ +function _wp_emoji_settings_script_module_data( array $data ): array { + return _wp_get_emoji_settings(); +} + +/** + * Enqueues the Emoji detection script module. + * + * @ignore + * @since 4.6.0 + * @deprecated 7.1.0 Use {@see _wp_enqueue_emoji_detection_script()} instead. + * @access private + */ +function _print_emoji_detection_script() { + _wp_enqueue_emoji_detection_script(); } /** diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index f16a51cb8b62c..3c93813c1dc17 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -9,20 +9,64 @@ class Tests_Formatting_Emoji extends WP_UnitTestCase { private $png_cdn = 'https://s.w.org/images/core/emoji/17.0.2/72x72/'; private $svg_cdn = 'https://s.w.org/images/core/emoji/17.0.2/svg/'; + /** + * @var WP_Script_Modules|null + */ + private $original_script_modules; + + /** + * @inheritDoc + */ + public function set_up() { + global $wp_script_modules; + + parent::set_up(); + + $this->original_script_modules = $wp_script_modules; + $wp_script_modules = null; + wp_script_modules(); + } + + /** + * @inheritDoc + */ + public function tear_down() { + global $wp_script_modules; + + $wp_script_modules = $this->original_script_modules; + parent::tear_down(); + } + + /** + * Returns the markup printed for the emoji detection script module. + * + * @return string Emoji detection script module markup. + */ + private function get_emoji_detection_script_output() { + // `_wp_enqueue_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: + self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); + + _wp_enqueue_emoji_detection_script(); + + return get_echo( array( wp_script_modules(), 'print_script_module_data' ) ) . + get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + } + /** * @ticket 63842 + * @ticket 64259 * - * @covers ::_print_emoji_detection_script + * @covers ::_wp_enqueue_emoji_detection_script + * @covers ::_wp_get_emoji_settings + * @covers ::_wp_emoji_settings_script_module_data */ public function test_script_tag_printing() { - // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: - self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); - $output = get_echo( '_print_emoji_detection_script' ); + $output = $this->get_emoji_detection_script_output(); $processor = new WP_HTML_Tag_Processor( $output ); $this->assertTrue( $processor->next_tag() ); $this->assertSame( 'SCRIPT', $processor->get_tag() ); - $this->assertSame( 'wp-emoji-settings', $processor->get_attribute( 'id' ) ); + $this->assertSame( 'wp-script-module-data-wp-emoji-loader', $processor->get_attribute( 'id' ) ); $this->assertSame( 'application/json', $processor->get_attribute( 'type' ) ); $text = $processor->get_modifiable_text(); $settings = json_decode( $text, true ); @@ -42,19 +86,19 @@ public function test_script_tag_printing() { $this->assertTrue( $processor->next_tag() ); $this->assertSame( 'SCRIPT', $processor->get_tag() ); $this->assertSame( 'module', $processor->get_attribute( 'type' ) ); - $this->assertNull( $processor->get_attribute( 'src' ) ); + $this->assertSame( 'low', $processor->get_attribute( 'fetchpriority' ) ); + $this->assertStringContainsString( 'wp-emoji-loader.js', (string) $processor->get_attribute( 'src' ) ); $this->assertFalse( $processor->next_tag() ); } /** * @ticket 36525 + * @ticket 64259 * - * @covers ::_print_emoji_detection_script + * @covers ::_wp_enqueue_emoji_detection_script */ public function test_unfiltered_emoji_cdns() { - // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: - self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); - $output = get_echo( '_print_emoji_detection_script' ); + $output = $this->get_emoji_detection_script_output(); $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); @@ -66,17 +110,16 @@ public function _filtered_emoji_svg_cdn( $cdn = '' ) { /** * @ticket 36525 + * @ticket 64259 * - * @covers ::_print_emoji_detection_script + * @covers ::_wp_enqueue_emoji_detection_script */ public function test_filtered_emoji_svn_cdn() { $filtered_svn_cdn = $this->_filtered_emoji_svg_cdn(); add_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svg_cdn' ) ); - // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: - self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); - $output = get_echo( '_print_emoji_detection_script' ); + $output = $this->get_emoji_detection_script_output(); $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); $this->assertStringNotContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); @@ -91,17 +134,16 @@ public function _filtered_emoji_png_cdn( $cdn = '' ) { /** * @ticket 36525 + * @ticket 64259 * - * @covers ::_print_emoji_detection_script + * @covers ::_wp_enqueue_emoji_detection_script */ public function test_filtered_emoji_png_cdn() { $filtered_png_cdn = $this->_filtered_emoji_png_cdn(); add_filter( 'emoji_url', array( $this, '_filtered_emoji_png_cdn' ) ); - // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: - self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); - $output = get_echo( '_print_emoji_detection_script' ); + $output = $this->get_emoji_detection_script_output(); $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );