-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Optimize emoji loader #4562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize emoji loader #4562
Conversation
|
This may be my silliness, but if Maybe we continue testing for new Unicode versions, but right now I don't think it's possible to have |
|
@dmsnell This would be better known by @pento, but I recall he said that emoji are part of the OS and not part of the browser. So even though the browser may support all the latest web platform features, it may still lack support for all emoji. See this comment of his. |
this is completely true, but I think no browser that supports these features can practically run on an OS without support for the versions of Unicode that we check for. maybe I'm wrong. |
I haven't looked in to this but even if it's the case at the time of writing, it won't remain so for long. Unicode 15.1 is scheduled for release in September 2023 at which time WP will update the emoji it checks for. |
src/js/_enqueues/lib/emoji-loader.js
Outdated
| sessionSupports[tests[ii]] = await browserSupportsEmojiOptimized( | ||
| tests[ii] | ||
| ); | ||
| settings.supports[tests[ii]] = sessionSupports[tests[ii]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case this is false, couldn't we stop the loop? Since there's no way settings.supports.everything will be true if the first execution of the loop results in settings.supports.everything = false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first glance that seems to be correct, but it seems we need to keep looping over everything because we need to set everythingExceptFlag properly. At second glance, this doesn't also seem to be needed, but I did a quick ack and found it is used in emoji.js:
| if ( settings.supports.everythingExceptFlag && |
So we still do need to loop over everything, I believe.
|
I also made an updated version of the file without using only promises: |
|
This is working for me. Without these changes, it took |
Thanks for that reference. There were two reasons why I went with
|
@valterlorran Thanks for verifying. As expected, since you have a powerful machine the time required to run emoji-loader is only ~10ms, which doesn't even qualify as a long task (unlike in my case when there is a long task on an HP Dragonfly Elite Chromebook running an Intel i7 processor). Nevertheless, even for a powerful machine there is still a 70% reduction of main thread work for the non-cached state and a 97% reduction for the cached state. |
|
According to WordPress's Browser support, those supported are:
And for IE, the results are:
So all IE browser versions combined account for less than 1%. Even less than 0.5%. Remarkable how far we've come! It seems we can truly abandon consideration for IE! |
felixarntz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter Note that while running a benchmark (see https://core.trac.wordpress.org/ticket/58472#comment:16), I ran into an issue where the WP core build workflow seemingly has a problem when minifying the changed file with your changes here present. It results in a JS console error when the minified code is loaded.
src/js/_enqueues/lib/emoji-loader.js
Outdated
| [ | ||
| emojiSetsRenderIdentically.toString() + | ||
| browserSupportsEmoji.toString() + | ||
| `postMessage(browserSupportsEmoji(${JSON.stringify(type)}))`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is problematic since the function name will change when the code is minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's a great point. Thanks for catching that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be fixed with 44c09ee
…into optimize/emoji-loader * 'trunk' of https://github.com/WordPress/wordpress-develop: (44 commits) Themes: Inline render blocking CSS `classic-themes.css' Bundled Themes: Remove `load_theme_textdomain()` calls from default themes. I18N: Allow to short-circuit `load_textdomain()`. Coding Standards: Use strict comparison in `wp-includes/pomo/entry.php`. Themes: add wp_get_remote_theme_patterns function. Tests/Build tools: Various term related test improvements. Posts, Post Types: Introduce `item_trashed` post type label. Tests/Build tools: Add `@covers` annotation to `wp_set_object_terms()` tests. Taxonomy: Prevent deprecation notices clearing terms. Build/Test Tools: Revert accidental change to .env Media: Update admin image editor design. Docs: Use third-person singular verbs in various function descriptions, as per docblocks standards. Docs: Use third-person singular verbs in various function descriptions, as per docblocks standards. Docs: Use third-person singular verbs in various function descriptions, as per docblocks standards. Fix lint issues in WP_Theme_JSON::sanitize method. Coding Standards: Use strict comparison in `wp-includes/class-wp-oembed.php`. Twenty Seventeen: Improve Grid View variation rendering in the editor for the Post List block. Ignore unregistered block style variations from `theme.json`. Docs: Use third-person singular verbs in various function descriptions, as per docblocks standards. Editor: Skip file_exist check for core blocks. ...
src/js/_enqueues/lib/emoji-loader.js
Outdated
| * @param {Document} document | ||
| * @param {WPEmojiSettings} settings | ||
| */ | ||
| ( async function( window, document, settings ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm getting an error here:
Running "jsvalidate:build" (jsvalidate) task
Validating build/wp-includes/js/wp-emoji-loader.js
>> Line 22: Unexpected token function
But I'm not immediately clear why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be due to Esprima not supporting ECMAScript 2017 yet? Or maybe there's a way to enable support in the esprimaOptions config:
wordpress-develop/Gruntfile.js
Lines 1038 to 1043 in 3e2121c
| jsvalidate:{ | |
| options: { | |
| globals: {}, | |
| esprimaOptions:{}, | |
| verbose: false | |
| }, |
I haven't been able to quickly determine what the possible options are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version of grunt-jsvalidate used in core is 0.2.2, which is the latest version available. It was last updated 10 years ago. What's more is that the GitHub repo has been archived. So clearly we need to use something else to check for syntax errors in JS files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I responded to your question about this in slack, but it may also be time for us to consider removing this validation step entirely at this point. Seems like perhaps this old validation task is causing more friction in our build process than it is helping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the sake of expediency for beta1, I'm going to try to make this ES3-compatible and then we can revisit taking advantage of ES6+.
peterwilsoncc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of notes inline.
Testing notes using a browser that doesn't require the polyfill:
- ran script, populating sessionStorage
- replaced all the test strings with the word
Gary(to emulate a required polyfill) - reloaded the browser: polyfill didn't load (as expected)
- ran
sessionStorage.clear()in the console - reloaded the browser: polyfill did load (as expected)
- reverted to committed version
- reloaded the browser: polyfill did load (as expected)
- ran
sessionStorage.clear()in the console - reloaded the browser: polyfill didn't load (as expected)
src/js/_enqueues/lib/emoji-loader.js
Outdated
| * | ||
| * @param {SupportTests} supports Supports. | ||
| */ | ||
| function setSessionSupports( supports ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To account for users who never close their browser/constantly duplicate tabs of their CMS, it it worth including a hash (or similar) of the values been passed to emojiSetsRenderIdentically? A time stamp might be an alternative to retest every 24-48 hours.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's necessary because presumably any change to the platform emoji support would require a reboot, and that would result in a new session, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems that may not be correct. If rebooting and browser tabs are restored, then the sessionStorage would need to persist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made this change in d694445
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data stored in sessionStorage is only considered if it isn't older than a day.
src/js/_enqueues/lib/emoji-loader.js
Outdated
| document.addEventListener( 'DOMContentLoaded', resolve, { | ||
| once: true | ||
| } ); | ||
| } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is also not a big deal, but it's curious that we interrupt the sessionSupportsPromise to create the domReadyPromise here. if we put domReadyPromise above sessionSupportsPromise couldn't we eliminate the creation of a variable for sessionSupportsPromise entirely and leave it as a single continuous chain instead of trapping a promise and then "re-starting" it in the code later?
var domReadyPromise = new Promise( … );
new Promise( function ( resolve ) {
…
} )
.then( function ( sessionSupports ) {
} )
.then( function () {
return domReadyPromise;
} )
.then( function () {
…
} );There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there intentionally no error-handling here? if any of the Promise callbacks throw then nothing happens and settings aren't updated.
would it be worth adding a final "global" .catch() as a catch-all "we couldn't determine support so support isn't guaranteed"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is also not a big deal, but it's curious that we interrupt the
sessionSupportsPromiseto create thedomReadyPromisehere.
I thought maybe it was easier to read, but apparently not. So I've made the change in aaf5718
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there intentionally no error-handling here? if any of the Promise callbacks throw then nothing happens and
settingsaren't updated.would it be worth adding a final "global"
.catch()as a catch-all "we couldn't determine support so support isn't guaranteed"?
There are try/catch blocks around code that I expect might throw errors. In the case where an error does occur, the result would be Twemoji wouldn't be loaded. I don't know if there should be a different outcome?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are try/catch blocks around code that I expect might throw errors.
makes sense. I guess we have to trust that the code inside those Promises won't throw. if for some reason they would (such as the unexpected bug with invalid code points) we would get an error about an unhandled catch, I think.
I don't know if there should be a different outcome?
maybe the only outcome is swallowing the errors and doing any necessary cleanup. if leaving session storage untouched is all the cleanup that's required, that makes sense. I have assumed that the failure mode is to make sure that twemoji and other scripts load, if we cannot determine that they aren't needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current emoji loader logic in trunk doesn't have any such error handling to force Twemoji to load in the case of an exception while gathering settings. I think we can leave that for future consideration.
…into optimize/emoji-loader * 'trunk' of https://github.com/WordPress/wordpress-develop: Menus: Allow themes and plugins to pass HTML attributes to various Nav Walker outputs. Database: Move the if statement outside of the loop. Editor: update Wordpress npm packages. Script Loader: Fix performance issues in `wp_common_block_scripts_and_styles`. Editor: allow filtering block patterns by source. Twenty Nineteen: Always set background color and foreground color together. Options, Meta APIs: Prime network options in a single cache call using wp_cache_get_multiple. Themes: Block template is located twice in `get_query_template()`. General: add block theme previews. Editor: refactor and stabilize selectors API. Twenty Nineteen: Add fragment ID to paginated links. Site Health: Add server time debug data. Editor: stabilise layout and refactor definitions. Editor: navigation post preloading. Editor: fix post edit navigation link. Editor: add navigation fallback. REST API: ignore empty templates. Upgrade/Install: Pass the full database version string to WordPress.org for parsing.
src/js/_enqueues/lib/emoji-loader.js
Outdated
| if ( | ||
| typeof item === 'object' && | ||
| typeof item.timestamp === 'number' && | ||
| new Date().valueOf() < item.timestamp + 86400 && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is ultimately tied to the version of Twemoji that WordPress ships isn't it? I think that the fallback case we're discussing is somewhat benign: if someone reboots and re-opens tabs and now their OS adds additional support for Unicode 14 Emoji, the worst thing that happens is that we continue to replace them until those tabs or the session storage clears. That's not that bad.
With this change we are going back to computing this at least once per day, which for many sites could very well be every page load.
Is the risky scenario that we update Twemoji in Core and can handle the next set of Emoji additions, but because this script cached the results it doesn't replace them and then the page is rendered improperly?
In that case could we build in some sentinel indicating the Twemoji version and then handle this deterministically? Update automatically when Twemoji updates or in the natural flow of browser reloads? Otherwise be okay with being a bit too cautious for some amount of time after Unicode releases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the fallback case we're discussing is somewhat benign: if someone reboots and re-opens tabs and now their OS adds additional support for Unicode 14 Emoji, the worst thing that happens is that we continue to replace them until those tabs or the session storage clears. That's not that bad.
Correct.
this is ultimately tied to the version of Twemoji that WordPress ships isn't it?
I don't think it's tied to the version of Twemoji but rather to whatever tests are present in browserSupportsEmoji(), which are [ 'flag', 'emoji' ]. I'm not familiar with the history of updating that function with new tests. Are you saying that instead of testing for a timestamp we should check to see if all of the tests are present? So if core adds another test we invalidate the cache to go ahead and compute the new results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should check to see if all of the tests are present?
actually this would be way better than what I suggested and wouldn't need to have any coupling with Twemoji. that's a brilliant idea, because the tests are trivial to serialize.
it wouldn't just be the names, but rather the specific strings we compare, or at least one of the two in comparison.
the ultimate goal is to use some canary string that we know is only supported in some version of the Unicode release. if that canary isn't present then we need Twemoji to fill in the gap.
if we don't add the test for it, the version of Twemoji wouldn't matter because we wouldn't be queuing it up anyway. supposing that Unicode releases a new version of the Emoji and a new character is available, we would need to add a new test to check if it renders as we expect. until then, the tests here would remain the same even though the rendering might be broken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the session storage cache is varied by the strings we compare, I don't think we can easily do so without computing a hash of the browserSupportsEmoji function and using this string for comparison. Perhaps a more efficient means would be to just introduce a emojiLoaderVersion that we bump whenever we touch the browserSupportsEmoji function to modify the strings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the cache should be invalidated whenever test strings change (when browserSupportsEmoji() is changed) or the OS emoji support changes. I think going ahead with just the 1 day expiration for now is adequate. We can consider further optimization after beta1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did so in b27e713.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you'd be happier with a week cache?
I'm happy with all you've done in this PR no matter how you choose the timeout. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter @dmsnell Sorry I didn't do a better job of explaining my thinking.
The thought behind the comment was indeed related to the twemoji (or WP version). I was thinking of the following situation:
- A user opens the browser, sessionStorage is set
- The user leaves the browser open overnight
- The site admins update WP to a version that includes an update to twemoji and the related sniffs
- The user returns and continues in the existing tab, as they navigate WP scripts will use the new version
- Without an expiration control the sniff would continue to be based on the previous tests.
Dennis, you raise a good point that the stale check could be based on the twemoji or WP version. That would allow the sessionStorage to remain valid until a potential change is detected
The origin of this thought was when I worked for a news organisation and discovered that journos keep their browsers open forever, constantly duplicating tabs. The team I was on was using both session and local storage and we had to add some code to invalidate it upon a deploy. Several years later, this experience continues to be traumatic. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Several years later, this experience continues to be traumatic. :)
That's understandable. Also why I like finding ways to make durable cache keys (for instance, "properly renders Emoji added in Unicode 12"
Been burnt the other way too, with "we'll only leave in daily expiration until the project stablizes…five years later" 😆
It's like cache invalidation is hard or something.
Honestly I'd love to look into redoing this after @westonruter suggested storing the test names as the key. Twemoji incorporates what looks like a 13 KB regular expression pattern that includes the entire database of replacements up through Unicode 14 (curious if and when they update to Unicode 15). If these were separated out we could potentially cut out 10 KB+ of download when required, and our session cache would remain durable under all but the most extreme edge cases (someone downgrades their operating system and re-opens a browser on the same tabs and is also not okay with missing Emoji)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've opened a Trac ticket we can use to track this going forward: Core-58663. I've marked it very low priority.
…into optimize/emoji-loader * 'trunk' of https://github.com/WordPress/wordpress-develop: Media: Only show “Copy” and “Download” actions when an attachment URL is available. Users: Introduce the `wp_update_user` action. Coding Standards: Fix a PHPCS issue found in `wpPluginsListTable.php`. Filesystem API: Allow optional inclusion of hidden files in `list_files()`. Plugins: Introduce the `plugins_list` filter.
felixarntz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code-wise this looks solid to me, and I just conducted another benchmark (results in https://core.trac.wordpress.org/ticket/58472#comment:23), happy to report this is still extremely impactful for LCP performance 🎉
Let's fix any of the last minor issues and hopefully get this committed.
It should be even more so now that it's only making one call to the worker, which means there's only overhead for one worker instance and the canvas only has to be constructed once. |
Makes sense, that explains why it's now ~20% LCP improvement while before it was ~15% :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter pardon my ignorance, but I'm still a bit lost on the fallback when things fail. for instance, in the previous script, as things run, if none of the tests complete, we end up loading the Twemoji script.
in this branch, however, if the browser doesn't support Promise (which would be a strong indicator that it likely also doesn't support updated Unicode revisions) then we abort immediately and never load the scripts (when we probably ought to).
likewise, even though we have try/catch wrappers around the constituent functions inside the large Promise chain, the only time we load the Twemoji scripts are when the chain finishes successfully. if some bug appears and we throw/catch in there, we don't fall back to loading the scripts.
am I still overlooking something obvious here, where a failure to get to the end of the testing code results in the browser loading the Emoji helpers?
|
Committed to |
What I mean is that if |
|
Oops. I think we may have merged this when it's broken for some of the very use-cases it's here for 🙃 |
The new code aborts immediately if |
WordPress doesn't support IE11 anymore, and this is the only browser that doesn't support promises. This PR was going to go all the way and use ES8+ syntax and break IE11 entirely, but for the sake of expediency ES3 compat was maintained so IE11 won't have a syntax error. See Trac comment. |
|
okay. we actually went through the unsupported browser discussion before and ended up not trying to go out of our way to support unsupported browsers. the |
Changes:
sessionStorageto remember the previous results of calls tobrowserSupportsEmoji().OffscreenCanvasis available, offloadbrowserSupportsEmoji()to a web worker to prevent blocking the main thread. This is of primary benefit to Safari which does not yet support thewillReadFrequentlyto the 2D canvas context.Todo:
Drop support for IE11?Modernize script withconst,let, arrow functions, etc.Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequentlybrowserSupportsEmoji()Find a modern alternative togrunt-jsvalidatewhich supports ES6+.Flesh out ESLint config for core?Trac ticket: https://core.trac.wordpress.org/ticket/58472
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.