#63842 closed enhancement (fixed)
Emoji detection inline script introduces needless parser/render blocking
Reported by: |
|
Owned by: |
|
---|---|---|---|
Milestone: | 6.9 | Priority: | normal |
Severity: | normal | Version: | 4.6 |
Component: | Emoji | Keywords: | has-patch has-unit-tests |
Focuses: | javascript, performance | Cc: |
Description (last modified by )
The inline script used for emoji detection, output in the head
by _print_emoji_detection_script()
, needlessly adds parser/render blocking to the page. If emoji loader already waits for DOMContentLoaded
before it proceeds to load the add load the wp-emoji.js
and twemoji.js
scripts. This means the inline script need not be in the head
at all. It could be moved to the footer, and it would also be better if it were added as an inline script module so that its execution is deferred until the DOM has loaded. Similarly, the emoji settings object exported from PHP need not be added as an inline script but it can instead be output as JSON (as is the method employed for script modules) to be parsed when the emoji detection script module executes.
This is closely related to #58472 which eliminated a long task in the emoji loader which caused significant render blocking on low-tier devices.
Change History (27)
This ticket was mentioned in PR #9531 on WordPress/wordpress-develop by @westonruter.
2 months ago
#1
- Keywords has-patch added
#2
@
2 months ago
- Description modified (diff)
- Focuses javascript added
- Owner set to westonruter
- Status changed from new to accepted
This ticket was mentioned in Slack in #core by westonruter. View the logs.
2 months ago
#6
@
2 months ago
Here's a polyfill plugin that implements this change for testing and early access: https://gist.github.com/westonruter/6b201c637e40d6d3b865e5cf7548c59e
@peterwilsoncc commented on PR #9531:
2 months ago
#7
I've noticed a back-compat issue for scripts that depend on the global settings object.
add_action( 'wp_enqueue_scripts', function() {
// Script that uses _wpemojiSettings. Something GDPR maybe?
wp_register_script( 'pwcc-no-deps-head', false, [], '1.0' );
wp_enqueue_script( 'pwcc-no-deps-head' );
wp_add_inline_script( 'pwcc-no-deps-head', 'console.log( "pwcc-no-deps-head", window._wpemojiSettings );' );
wp_register_script( 'pwcc-no-deps-foot', false, [], '1.0', true );
wp_enqueue_script( 'pwcc-no-deps-foot' );
wp_add_inline_script( 'pwcc-no-deps-foot', 'console.log( "pwcc-no-deps-foot", window._wpemojiSettings );' );
} );
On trunk
both of these scripts log the settings object, on this branch they both log undefined
.
Searching the plugin repo for _wpemojiSettings shows that it's referenced in WooCommerce Payments, 700K active sites in the file registered by this block of code, so I think we'll need some care if this is to change.
A few of the other results appear to be false positives.
@westonruter commented on PR #9531:
2 months ago
#8
Here's where _wpemojiSettings
is referenced in the plugin:
Specifically it's I'm a block's edit function in the editor, which would surely be executed after DOMContentLoaded
, so it doesn't seem like it would be a problem.
This ticket was mentioned in Slack in #core-test by oglekler. View the logs.
7 weeks ago
#10
@
7 weeks ago
- Keywords needs-testing removed
I am removing needs-testing due to this comment: https://github.com/WordPress/wordpress-develop/pull/9531/files#r2288862656 with relation to #58873
Let's wait until the patch is really ready. Thank you!
@westonruter commented on PR #9531:
3 weeks ago
#11
@peterwilsoncc:
I've noticed a back-compat issue for scripts that depend on the global settings object.
In regards to other plugins which may be directly referencing the _wpemojiSettings
global (ill-advisedly), I think the best approach here will be to check to see if it is set, and if not, read it from the JSON script. Note that WooCommerce is already checking for whether or not the global exists:
/** * WP Emoji replaces the flag emoji with an image if it's not natively * supported by the browser. This behavior is problematic on Windows * because it renders an <img> tag inside the <option>, which can lead to crashes. * We need to guarantee that the OS supports flag emojis before rendering it. */ const supportsFlagEmoji = window._wpemojiSettings ? window._wpemojiSettings.supports?.flag : true;
So it could simply be modified to do something like:
let wpEmojiSettings = window._wpemojiSettings; if ( ! wpEmojiSettings ) { const settingsScript = document.getElementById( 'wp-emoji-settings' ); if ( settingsScript ) { wpEmojiSettings = JSON.parse( settingsScript.text ); } } const supportsFlagEmoji = wpEmojiSettings ? wpEmojiSettings.supports?.flag : true;
Note that it is likely that any script attempting to access window._wpemojiSettings
would have run after the module had been evaluated anyway, because modules execute before DOMContentLoaded
. So the above back-compat code would likely not be needed anyway.
I think any compatibility issues can be addressed by a dev note and outreach.
#12
@
3 weeks ago
- Keywords has-unit-tests added; needs-unit-tests removed
I've marked this as ready for review.
Note there are two related tickets that will touch this code:
- #58873: Add function to pass variables to scripts (where a new API could be leveraged to print the JSON script).
- #63887: Add sourceURL to inline scripts and styles (namely this PR).
I don't see these as blockers, however, since this PR is closest to completion. They'll need to account for the change here, however.
@westonruter commented on PR #9531:
3 weeks ago
#13
This seems like a nice improvement. I tested and didn't find any issues, but I don't actually know how to test the functionality of wpemoji. Do you know what behaviors I should look for? I'm happy to come back and confirm that things are working as expected.
@sirreal Good question. I usually resort to patching the JS to force Twemoji to run and to never use the sessionStorage
cache, for example with the following change applied to this PR:
-
src/js/_enqueues/lib/emoji-loader.js
a b function emojiRendersEmptyCenterPoint( context, emoji ) { 216 216 * @return {boolean} True if the browser can render emoji, false if it cannot. 217 217 */ 218 218 function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { 219 return false; 219 220 let isIdentical; 220 221 221 222 switch ( type ) { … … const domReadyPromise = new Promise( ( resolve ) => { 370 371 371 372 // Obtain the emoji support from the browser, asynchronously when possible. 372 373 new Promise( ( resolve ) => { 373 let supportTests = getSessionSupportTests(); 374 if ( supportTests ) { 375 resolve( supportTests ); 376 return; 377 } 374 // let supportTests = getSessionSupportTests(); 375 // if ( supportTests ) { 376 // resolve( supportTests ); 377 // return; 378 // } 379 let supportTests = null; 378 380 379 381 if ( supportsWorkerOffloading() ) { 380 382 try {
@westonruter commented on PR #9531:
3 weeks ago
#14
OK, I've re-run the metrics.
First, the re-obtaining results comparing trunk
(classic blocking inline script) with an inline script module (deferred):
URL | Before | After | Diff (ms) | Diff (%)
-- | --: | --: | --: | --:
FCP (median) | 322.4 | 300.5 | -21.9 | -6.79%
LCP (median) | 407.4 | 391.9 | -15.6 | -3.82%
TTFB (median) | 58.4 | 59.0 | 0.6 | 1.11%
LCP-TTFB (median) | 348.7 | 333.4 | -15.4 | -4.40%
And then here is are the results comparing trunk
with an inline script module with async
:
URL | Before | After | Diff (ms) | Diff (%)
-- | --: | --: | --: | --:
FCP (median) | 322.4 | 323.0 | 0.6 | 0.20%
LCP (median) | 407.4 | 403.1 | -4.3 | -1.07%
TTFB (median) | 58.4 | 58.4 | 0.0 | 0.09%
LCP-TTFB (median) | 348.7 | 343.4 | -5.3 | -1.52%
<details><summary>Raw Results</summary>
These results were obtained via multiple calls to with different states of wordpress-develop
checked out:
npm run research -- benchmark-web-vitals --url http://localhost:8000/sample-page/?enable_plugins=none --output=csv --number=100 --throttle-cpu=20
trunk:
URL,http://localhost:8000/sample-page/?enable_plugins=none Success Rate,100% FCP (median),322.35 LCP (median),407.4 TTFB (median),58.35 LCP-TTFB (median),348.7
module (with defer):
URL,http://localhost:8000/sample-page/?enable_plugins=none Success Rate,100% FCP (median),300.45 LCP (median),391.85 TTFB (median),59 LCP-TTFB (median),333.35
module with async:
URL,http://localhost:8000/sample-page/?enable_plugins=none Success Rate,100% FCP (median),323 LCP (median),403.05 TTFB (median),58.4 LCP-TTFB (median),343.4
</details>
So in terms of LCP, it does seem like it would be better to actually remove async
, which naturally makes sense. And it's logical to do as well, since it is extremely unlikely that an emoji will end up being the LCP element. Therefore, it makes sense to me that we should actually _not_ use async
, and go ahead and let the emoji detection run after the DOM has loaded. Note that the emoji tests are cached in sessionStorage
as well, which means subsequent page loads won't have to wait for the worker.
@jonsurrell commented on PR #9531:
3 weeks ago
#15
Therefore, it makes sense to me that we should actually not use async
Agreed. The fact that the script waited for DOMContentLoaded also suggests it was a good candidate for defer
or the default module behavior like in this PR 👍
@westonruter commented on PR #9531:
3 weeks ago
#16
I just noticed an issue: because the IIFE was removed, the minification process wasn't able to reduce the length of the top-level symbols because it isn't aware that it is a module.
# Before
<script> window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/16.0.1\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/16.0.1\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/make.wordpress.org\/wp-includes\/js\/wp-emoji-release.min.js?ver=6.9-alpha-60888"}}; /*! This file is auto-generated */ !function(s,n){var o,i,e;function c(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function p(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data),a=(e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0),new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data));return t.every(function(e,t){return e===a[t]})}function u(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);for(var n=e.getImageData(16,16,1,1),a=0;a<n.data.length;a++)if(0!==n.data[a])return!1;return!0}function f(e,t,n,a){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!a(e,"\ud83e\udedf")}return!1}function g(e,t,n,a){var r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):s.createElement("canvas"),o=r.getContext("2d",{willReadFrequently:!0}),i=(o.textBaseline="top",o.font="600 32px Arial",{});return e.forEach(function(e){i[e]=t(o,e,n,a)}),i}function t(e){var t=s.createElement("script");t.src=e,t.defer=!0,s.head.appendChild(t)}"undefined"!=typeof Promise&&(o="wpEmojiSettingsSupports",i=["flag","emoji"],n.supports={everything:!0,everythingExceptFlag:!0},e=new Promise(function(e){s.addEventListener("DOMContentLoaded",e,{once:!0})}),new Promise(function(t){var n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+g.toString()+"("+[JSON.stringify(i),f.toString(),p.toString(),u.toString()].join(",")+"));",a=new Blob([e],{type:"text/javascript"}),r=new Worker(URL.createObjectURL(a),{name:"wpTestEmojiSupports"});return void(r.onmessage=function(e){c(n=e.data),r.terminate(),t(n)})}catch(e){}c(n=g(i,f,p,u))}t(n)}).then(function(e){for(var t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=function(){n.DOMReady=!0}}).then(function(){return e}).then(function(){var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}))}((window,document),window._wpemojiSettings); </script>
JS script byte length: 3,399 bytes
# After:
<script id="wp-emoji-settings" type="application/json"> {"baseUrl":"https://s.w.org/images/core/emoji/16.0.1/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/16.0.1/svg/","svgExt":".svg","source":{"concatemoji":"http://localhost:8000/wp-includes/js/wp-emoji-release.min.js?ver=6.9-alpha-60093-src"}} </script> <script type="module"> /*! This file is auto-generated */ const settings=JSON.parse(document.getElementById("wp-emoji-settings").textContent),sessionStorageKey=(window._wpemojiSettings=settings,"wpEmojiSettingsSupports"),tests=["flag","emoji"];function supportsWorkerOffloading(){return"undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob}function getSessionSupportTests(){try{var t=JSON.parse(sessionStorage.getItem(sessionStorageKey));if("object"==typeof t&&"number"==typeof t.timestamp&&(new Date).valueOf()<t.timestamp+604800&&"object"==typeof t.supportTests)return t.supportTests}catch(t){}return null}function setSessionSupportTests(t){try{var e={supportTests:t,timestamp:(new Date).valueOf()};sessionStorage.setItem(sessionStorageKey,JSON.stringify(e))}catch(t){}}function emojiSetsRenderIdentically(t,e,s){t.clearRect(0,0,t.canvas.width,t.canvas.height),t.fillText(e,0,0);e=new Uint32Array(t.getImageData(0,0,t.canvas.width,t.canvas.height).data);t.clearRect(0,0,t.canvas.width,t.canvas.height),t.fillText(s,0,0);const n=new Uint32Array(t.getImageData(0,0,t.canvas.width,t.canvas.height).data);return e.every((t,e)=>t===n[e])}function emojiRendersEmptyCenterPoint(t,e){t.clearRect(0,0,t.canvas.width,t.canvas.height),t.fillText(e,0,0);var s=t.getImageData(16,16,1,1);for(let t=0;t<s.data.length;t++)if(0!==s.data[t])return!1;return!0}function browserSupportsEmoji(t,e,s,n){switch(e){case"flag":return s(t,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!s(t,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!s(t,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!n(t,"\ud83e\udedf")}return!1}function testEmojiSupports(t,e,s,n){let r;const o=(r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),i=(o.textBaseline="top",o.font="600 32px Arial",{});return t.forEach(t=>{i[t]=e(o,t,s,n)}),i}function addScript(t){var e=document.createElement("script");e.src=t,e.defer=!0,document.head.appendChild(e)}settings.supports={everything:!0,everythingExceptFlag:!0},new Promise(e=>{let s=getSessionSupportTests();if(!s){if(supportsWorkerOffloading())try{var t="postMessage("+testEmojiSupports.toString()+"("+[JSON.stringify(tests),browserSupportsEmoji.toString(),emojiSetsRenderIdentically.toString(),emojiRendersEmptyCenterPoint.toString()].join(",")+"));",n=new Blob([t],{type:"text/javascript"});const r=new Worker(URL.createObjectURL(n),{name:"wpTestEmojiSupports"});return void(r.onmessage=t=>{setSessionSupportTests(s=t.data),r.terminate(),e(s)})}catch(t){}setSessionSupportTests(s=testEmojiSupports(tests,browserSupportsEmoji,emojiSetsRenderIdentically,emojiRendersEmptyCenterPoint))}e(s)}).then(t=>{for(const e in t)settings.supports[e]=t[e],settings.supports.everything=settings.supports.everything&&settings.supports[e],"flag"!==e&&(settings.supports.everythingExceptFlag=settings.supports.everythingExceptFlag&&settings.supports[e]);settings.supports.everythingExceptFlag=settings.supports.everythingExceptFlag&&!settings.supports.flag,settings.DOMReady=!1,settings.readyCallback=()=>{settings.DOMReady=!0}}).then(()=>{var t;settings.supports.everything||(settings.readyCallback(),(t=settings.source||{}).concatemoji?addScript(t.concatemoji):t.wpemoji&&t.twemoji&&(addScript(t.twemoji),addScript(t.wpemoji)))}); //# sourceURL=http://localhost:8000/wp-includes/js/wp-emoji-loader.min.js </script>
JS script byte length: 3,708
@westonruter commented on PR #9531:
3 weeks ago
#17
OK, I've got it. With 589e11c31ab1ae47a2ea80ba0c282a2828bd7150 I've forced UglifyJS to consider the wp-emoji-loader.js
to be a module and to minify the top-level symbols. Now the result is 3,009 bytes, so even a tiny bit smaller than the original non-module JS at 3,055 bytes:
const n=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(window._wpemojiSettings=n,"wpEmojiSettingsSupports"),s=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const a=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===a[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e<n.data.length;e++)if(0!==n.data[e])return!1;return!0}function u(e,t,n,a){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!a(e,"\ud83e\udedf")}return!1}function f(e,t,n,a){let r;const o=(r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),s=(o.textBaseline="top",o.font="600 32px Arial",{});return e.forEach(e=>{s[e]=t(o,e,n,a)}),s}function t(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}n.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),c.toString(),p.toString()].join(",")+"));",a=new Blob([e],{type:"text/javascript"});const r=new Worker(URL.createObjectURL(a),{name:"wpTestEmojiSupports"});return void(r.onmessage=e=>{i(n=e.data),r.terminate(),t(n)})}catch(e){}i(n=f(s,u,c,p))}t(n)}).then(e=>{for(const t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=()=>{n.DOMReady=!0}}).then(()=>{var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}); //# sourceURL=http://localhost:8000/wp-includes/js/wp-emoji-loader.min.js
Aside: We should separately look into moving this script module to be printed at wp_footer
instead:
-
src/wp-includes/formatting.php
a b function print_emoji_detection_script() { 5912 5912 5913 5913 $printed = true; 5914 5914 5915 _print_emoji_detection_script();5915 add_action( 'wp_print_footer_scripts', '_print_emoji_detection_script' ); 5916 5916 } 5917 5917 5918 5918 /**
This would cut out the following 3,403 bytes of HTML from being needlessly in the head
, ensuring that what is actually critical (i.e. CSS) is parsed while the document is getting streamed (cc @dmsnell):
<script id="wp-emoji-settings" type="application/json"> {"baseUrl":"https://s.w.org/images/core/emoji/16.0.1/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/16.0.1/svg/","svgExt":".svg","source":{"concatemoji":"http://localhost:8000/wp-includes/js/wp-emoji-release.min.js?ver=6.9-alpha-60093-src"}} </script> <script type="module"> /*! This file is auto-generated */ const n=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(window._wpemojiSettings=n,"wpEmojiSettingsSupports"),s=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const a=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===a[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e<n.data.length;e++)if(0!==n.data[e])return!1;return!0}function u(e,t,n,a){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!a(e,"\ud83e\udedf")}return!1}function f(e,t,n,a){let r;const o=(r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),s=(o.textBaseline="top",o.font="600 32px Arial",{});return e.forEach(e=>{s[e]=t(o,e,n,a)}),s}function t(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}n.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),c.toString(),p.toString()].join(",")+"));",a=new Blob([e],{type:"text/javascript"});const r=new Worker(URL.createObjectURL(a),{name:"wpTestEmojiSupports"});return void(r.onmessage=e=>{i(n=e.data),r.terminate(),t(n)})}catch(e){}i(n=f(s,u,c,p))}t(n)}).then(e=>{for(const t in e)n.supports[t]=e[t],n.supports.everything=n.supports.everything&&n.supports[t],"flag"!==t&&(n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&n.supports[t]);n.supports.everythingExceptFlag=n.supports.everythingExceptFlag&&!n.supports.flag,n.DOMReady=!1,n.readyCallback=()=>{n.DOMReady=!0}}).then(()=>{var e;n.supports.everything||(n.readyCallback(),(e=n.source||{}).concatemoji?t(e.concatemoji):e.wpemoji&&e.twemoji&&(t(e.twemoji),t(e.wpemoji)))}); //# sourceURL=http://localhost:8000/wp-includes/js/wp-emoji-loader.min.js </script>
@westonruter commented on PR #9531:
3 weeks ago
#18
With the changes in this PR, I applied this patch:
-
src/wp-includes/formatting.php
a b function print_emoji_detection_script() { 5912 5912 5913 5913 $printed = true; 5914 5914 5915 _print_emoji_detection_script(); 5915 if ( isset( $_GET['print_emoji_detection_script_position'] ) && 'footer' === $_GET['print_emoji_detection_script_position'] ) { 5916 add_action( 'wp_print_footer_scripts', '_print_emoji_detection_script' ); 5917 } else { 5918 _print_emoji_detection_script(); 5919 } 5916 5920 } 5917 5921 5918 5922 /**
I then obtained the metrics for 100 requests for the script being printed in wp_head
vs wp_footer
over a Fast 4G emulated connection:
npm run research -- benchmark-web-vitals --url="http://localhost:8000/sample-page/?enable_plugins=none&print_emoji_detection_script_position=head" --url="http://localhost:8000/sample-page/?enable_plugins=none&print_emoji_detection_script_position=footer" --output=md --number=100 --network-conditions="Fast 4G" --diff
The results show a modest yet clear ~1% improvement to LCP and ~2% improvement to FCP:
Metric | wp_head | wp_footer | Diff (ms) | Diff (%) |
:------------------ | ----------: | --------------: | --------: | -------: |
FCP (median) | 429.1 | 421.4 | -7.70 | -1.8% |
LCP (median) | 526.3 | 519.9 | -6.40 | -1.2% |
TTFB (median) | 59.35 | 58.5 | -0.85 | -1.4% |
LCP-TTFB (median) | 466.35 | 461.8 | -4.55 | -1.0% |
@westonruter commented on PR #9531:
3 weeks ago
#19
I did for same while emulating Slow 3G and got the following results, 100 for wp_head
and 100 for wp_footer
. Still an improvement although about half as much as Fast 4G:
Metric | wp_head | wp_footer | Diff (ms) | Diff (%) |
:------------------ | ----------: | ------------: | ----------: | -------: |
FCP (median) | 4902.4 | 4870.9 | -31.55 | -0.6% |
LCP (median) | 4997.2 | 4972.9 | -24.30 | -0.5% |
TTFB (median) | 60.6 | 60.6 | +0.1 | +0.1% |
LCP-TTFB (median) | 4936.1 | 4912.7 | -23.40 | -0.5% |
#21
follow-up:
↓ 24
@
2 weeks ago
I think this change caused errors related to script modules on Firefox. Based on my research, I believe this change also caused E2E tests related to navigation block to start failing in the Gutenberg project.
See: https://github.com/WordPress/gutenberg/actions/workflows/end2end-test.yml
Now that the emoji loader is loaded via a script module, the source code looks like this:
<script type="module"> /** * @output wp-includes/js/wp-emoji-loader.js */ ... </script> <script type="importmap" id="wp-importmap"> {"imports":{"@wordpress/interactivity":"http://localhost:8889/wp-includes/js/dist/script-modules/interactivity/debug.js?ver=844167190aa4a0e411f0"}} </script> <script type="module" src="http://localhost:8889/wp-includes/js/dist/script-modules/block-library/navigation/view.js?ver=52594d8ac17824d31d5e" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
As you can see, the import map is defined after the emoji loader.
My understanding is that script modules are treated more strictly in Firefox than in Chrome, so running a script module before the import map will result in an error.
If you launch the latest wordpress-develop in Firefox, you should be able to see the error in the browser console.
I think we can enqueue _wpemojiSettings
with wp_print_inline_script_tag
, but we may be able to simply use wp_enqueue_script_module
for wp-emoji-loader.js
.
This ticket was mentioned in Slack in #core-editor by wildworks. View the logs.
2 weeks ago
#24
in reply to:
↑ 21
@
2 weeks ago
#25
@
2 weeks ago
I can confirm that PR #10145 (for #64076) fixes the issue in Firefox in both block themes and classic themes by ensuring that the emoji loader script module is printed after the importmap
script.
(I corrected the PR link.)
Trac ticket: https://core.trac.wordpress.org/ticket/63842
This splits up the inline script output in
_print_emoji_detection_script()
into two:script
of typeapplication/json
which contains the_wpemojiSettings
data.script
of typemodule
which containswp-emoji-loader.js
, which parses_wpemojiSettings
out of the JSON.The result is this inline script is eliminated from blocking the HTML parser from processing the page (and rendering the page) while waiting for the JavaScript to execute. The
wp-emoji-loader.js
script does not need to run in thehead
because it does not actually proceed with loading emoji (if needed) untilDOMContentLoaded
.I used the `benchmark-web-vitals` command from GoogleChromeLabs/wpp-research to analyze the performance impact of this on a vanilla WordPress install on the Sample Page. For example:
On a high-end machine (e.g. MacBook Pro with M4 Pro chip), the difference is negligible:
However, when the CPU is throttled (via
--throttle-cpu
) to emulate a low-tier mobile phone, then there is a clear impact. I used CPU throttle factor of 20 because this value is close to how Chrome DevTools calibrates my CPU to emulate such a device:The command I run on
trunk
and again on this branch:The results show a >5% improvement to LCP:
This is the diff or a rendered page with Prettier formatting applied and
SCRIPT_DEBUG
enabled:.html