Make WordPress Core

Changeset 56074


Ignore:
Timestamp:
06/27/2023 05:22:59 PM (3 years ago)
Author:
westonruter
Message:

Emoji: Optimize emoji loader with sessionStorage, willReadFrequently, and OffscreenCanvas.

  • Use sessionStorage to remember the previous results of calls to browserSupportsEmoji() for 1 week.
  • Optimize reading from canvas by supplying the willReadFrequently option for the 2D context.
  • When OffscreenCanvas is available, offload browserSupportsEmoji() to a web worker to prevent blocking the main thread. This is of primary benefit to Safari which does not yet support willReadFrequently.
  • Remove obsolete support for IE11 since promises are now utilized. Nevertheless, ES3 syntax is maintained and IE11 will simply short-circuit.

Props westonruter, dmsnell, peterwilsoncc, valterlorran, flixos90, spacedmonkey, joemcgill.
Fixes #58472.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/.jshintrc

    r56049 r56074  
    2525                "export": false,
    2626                "module": false,
    27                 "require": false
     27                "require": false,
     28                "WorkerGlobalScope": false,
     29                "self": false,
     30                "OffscreenCanvas": false,
     31                "Promise": false
    2832        }
    2933}
  • trunk/src/js/_enqueues/lib/emoji-loader.js

    r55241 r56074  
    33 */
    44
    5 ( function( window, document, settings ) {
    6         var src, ready, ii, tests;
    7 
    8         // Create a canvas element for testing native browser support of emoji.
    9         var canvas = document.createElement( 'canvas' );
    10         var context = canvas.getContext && canvas.getContext( '2d' );
     5/**
     6 * Emoji Settings as exported in PHP via _print_emoji_detection_script().
     7 * @typedef WPEmojiSettings
     8 * @type {object}
     9 * @property {?object} source
     10 * @property {?string} source.concatemoji
     11 * @property {?string} source.twemoji
     12 * @property {?string} source.wpemoji
     13 * @property {?boolean} DOMReady
     14 * @property {?Function} readyCallback
     15 */
     16
     17/**
     18 * Support tests.
     19 * @typedef SupportTests
     20 * @type {object}
     21 * @property {?boolean} flag
     22 * @property {?boolean} emoji
     23 */
     24
     25/**
     26 * IIFE to detect emoji support and load Twemoji if needed.
     27 *
     28 * @param {Window} window
     29 * @param {Document} document
     30 * @param {WPEmojiSettings} settings
     31 */
     32( function wpEmojiLoader( window, document, settings ) {
     33        if ( typeof Promise === 'undefined' ) {
     34                return;
     35        }
     36
     37        var sessionStorageKey = 'wpEmojiSettingsSupports';
     38        var tests = [ 'flag', 'emoji' ];
     39
     40        /**
     41         * Checks whether the browser supports offloading to a Worker.
     42         *
     43         * @since 6.3.0
     44         *
     45         * @private
     46         *
     47         * @returns {boolean}
     48         */
     49        function supportsWorkerOffloading() {
     50                return (
     51                        typeof Worker !== 'undefined' &&
     52                        typeof OffscreenCanvas !== 'undefined' &&
     53                        typeof URL !== 'undefined' &&
     54                        URL.createObjectURL &&
     55                        typeof Blob !== 'undefined'
     56                );
     57        }
     58
     59        /**
     60         * @typedef SessionSupportTests
     61         * @type {object}
     62         * @property {number} timestamp
     63         * @property {SupportTests} supportTests
     64         */
     65
     66        /**
     67         * Get support tests from session.
     68         *
     69         * @since 6.3.0
     70         *
     71         * @private
     72         *
     73         * @returns {?SupportTests} Support tests, or null if not set or older than 1 week.
     74         */
     75        function getSessionSupportTests() {
     76                if (
     77                        typeof sessionStorage !== 'undefined' &&
     78                        sessionStorageKey in sessionStorage
     79                ) {
     80                        try {
     81                                /** @type {SessionSupportTests} */
     82                                var item = JSON.parse(
     83                                        sessionStorage.getItem( sessionStorageKey )
     84                                );
     85                                if (
     86                                        typeof item === 'object' &&
     87                                        typeof item.timestamp === 'number' &&
     88                                        new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds.
     89                                        typeof item.supportTests === 'object'
     90                                ) {
     91                                        return item.supportTests;
     92                                }
     93                        } catch ( e ) {}
     94                }
     95                return null;
     96        }
     97
     98        /**
     99         * Persist the supports in session storage.
     100         *
     101         * @since 6.3.0
     102         *
     103         * @private
     104         *
     105         * @param {SupportTests} supportTests Support tests.
     106         */
     107        function setSessionSupportTests( supportTests ) {
     108                if ( typeof sessionStorage !== 'undefined' ) {
     109                        try {
     110                                /** @type {SessionSupportTests} */
     111                                var item = {
     112                                        supportTests: supportTests,
     113                                        timestamp: new Date().valueOf()
     114                                };
     115
     116                                sessionStorage.setItem(
     117                                        sessionStorageKey,
     118                                        JSON.stringify( item )
     119                                );
     120                        } catch ( e ) {}
     121                }
     122        }
    11123
    12124        /**
    13125         * Checks if two sets of Emoji characters render the same visually.
    14126         *
     127         * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     128         * scope. Everything must be passed by parameters.
     129         *
    15130         * @since 4.9.0
    16131         *
    17132         * @private
    18133         *
     134         * @param {CanvasRenderingContext2D} context 2D Context.
    19135         * @param {string} set1 Set of Emoji to test.
    20136         * @param {string} set2 Set of Emoji to test.
    … …  
    22138         * @return {boolean} True if the two sets render the same.
    23139         */
    24         function emojiSetsRenderIdentically( set1, set2 ) {
     140        function emojiSetsRenderIdentically( context, set1, set2 ) {
    25141                // Cleanup from previous test.
    26                 context.clearRect( 0, 0, canvas.width, canvas.height );
     142                context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
    27143                context.fillText( set1, 0, 0 );
    28                 var rendered1 = canvas.toDataURL();
     144                var rendered1 = new Uint32Array(
     145                        context.getImageData(
     146                                0,
     147                                0,
     148                                context.canvas.width,
     149                                context.canvas.height
     150                        ).data
     151                );
    29152
    30153                // Cleanup from previous test.
    31                 context.clearRect( 0, 0, canvas.width, canvas.height );
     154                context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
    32155                context.fillText( set2, 0, 0 );
    33                 var rendered2 = canvas.toDataURL();
    34 
    35                 return rendered1 === rendered2;
     156                var rendered2 = new Uint32Array(
     157                        context.getImageData(
     158                                0,
     159                                0,
     160                                context.canvas.width,
     161                                context.canvas.height
     162                        ).data
     163                );
     164
     165                return rendered1.every( function ( rendered2Data, index ) {
     166                        return rendered2Data === rendered2[ index ];
     167                } );
    36168        }
    37169
    … …  
    39171         * Determines if the browser properly renders Emoji that Twemoji can supplement.
    40172         *
     173         * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     174         * scope. Everything must be passed by parameters.
     175         *
    41176         * @since 4.2.0
    42177         *
    43178         * @private
    44179         *
     180         * @param {CanvasRenderingContext2D} context 2D Context.
    45181         * @param {string} type Whether to test for support of "flag" or "emoji".
    46182         *
    47183         * @return {boolean} True if the browser can render emoji, false if it cannot.
    48184         */
    49         function browserSupportsEmoji( type ) {
     185        function browserSupportsEmoji( context, type ) {
    50186                var isIdentical;
    51187
    52                 if ( ! context || ! context.fillText ) {
    53                         return false;
    54                 }
     188                switch ( type ) {
     189                        case 'flag':
     190                                /*
     191                                 * Test for Transgender flag compatibility. Added in Unicode 13.
     192                                 *
     193                                 * To test for support, we try to render it, and compare the rendering to how it would look if
     194                                 * the browser doesn't render it correctly (white flag emoji + transgender symbol).
     195                                 */
     196                                isIdentical = emojiSetsRenderIdentically(
     197                                        context,
     198                                        '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence
     199                                        '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space
     200                                );
     201
     202                                if ( isIdentical ) {
     203                                        return false;
     204                                }
     205
     206                                /*
     207                                 * Test for UN flag compatibility. This is the least supported of the letter locale flags,
     208                                 * so gives us an easy test for full support.
     209                                 *
     210                                 * To test for support, we try to render it, and compare the rendering to how it would look if
     211                                 * the browser doesn't render it correctly ([U] + [N]).
     212                                 */
     213                                isIdentical = emojiSetsRenderIdentically(
     214                                        context,
     215                                        '\uD83C\uDDFA\uD83C\uDDF3', // as the sequence of two code points
     216                                        '\uD83C\uDDFA\u200B\uD83C\uDDF3' // as the two code points separated by a zero-width space
     217                                );
     218
     219                                if ( isIdentical ) {
     220                                        return false;
     221                                }
     222
     223                                /*
     224                                 * Test for English flag compatibility. England is a country in the United Kingdom, it
     225                                 * does not have a two letter locale code but rather a five letter sub-division code.
     226                                 *
     227                                 * To test for support, we try to render it, and compare the rendering to how it would look if
     228                                 * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]).
     229                                 */
     230                                isIdentical = emojiSetsRenderIdentically(
     231                                        context,
     232                                        // as the flag sequence
     233                                        '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F',
     234                                        // with each code point separated by a zero-width space
     235                                        '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F'
     236                                );
     237
     238                                return ! isIdentical;
     239                        case 'emoji':
     240                                /*
     241                                 * Why can't we be friends? Everyone can now shake hands in emoji, regardless of skin tone!
     242                                 *
     243                                 * To test for Emoji 14.0 support, try to render a new emoji: Handshake: Light Skin Tone, Dark Skin Tone.
     244                                 *
     245                                 * The Handshake: Light Skin Tone, Dark Skin Tone emoji is a ZWJ sequence combining 🫱 Rightwards Hand,
     246                                 * 🏻 Light Skin Tone, a Zero Width Joiner, 🫲 Leftwards Hand, and 🏿 Dark Skin Tone.
     247                                 *
     248                                 * 0x1FAF1 == Rightwards Hand
     249                                 * 0x1F3FB == Light Skin Tone
     250                                 * 0x200D == Zero-Width Joiner (ZWJ) that links the code points for the new emoji or
     251                                 * 0x200B == Zero-Width Space (ZWS) that is rendered for clients not supporting the new emoji.
     252                                 * 0x1FAF2 == Leftwards Hand
     253                                 * 0x1F3FF == Dark Skin Tone.
     254                                 *
     255                                 * When updating this test for future Emoji releases, ensure that individual emoji that make up the
     256                                 * sequence come from older emoji standards.
     257                                 */
     258                                isIdentical = emojiSetsRenderIdentically(
     259                                        context,
     260                                        '\uD83E\uDEF1\uD83C\uDFFB\u200D\uD83E\uDEF2\uD83C\uDFFF', // as the zero-width joiner sequence
     261                                        '\uD83E\uDEF1\uD83C\uDFFB\u200B\uD83E\uDEF2\uD83C\uDFFF' // separated by a zero-width space
     262                                );
     263
     264                                return ! isIdentical;
     265                }
     266
     267                return false;
     268        }
     269
     270        /**
     271         * Checks emoji support tests.
     272         *
     273         * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     274         * scope. Everything must be passed by parameters.
     275         *
     276         * @since 6.3.0
     277         *
     278         * @private
     279         *
     280         * @param {string[]} tests Tests.
     281         *
     282         * @return {SupportTests} Support tests.
     283         */
     284        function testEmojiSupports( tests ) {
     285                var canvas;
     286                if (
     287                        typeof WorkerGlobalScope !== 'undefined' &&
     288                        self instanceof WorkerGlobalScope
     289                ) {
     290                        canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement.
     291                } else {
     292                        canvas = document.createElement( 'canvas' );
     293                }
     294
     295                var context = canvas.getContext( '2d', { willReadFrequently: true } );
    55296
    56297                /*
    … …  
    62303                context.font = '600 32px Arial';
    63304
    64                 switch ( type ) {
    65                         case 'flag':
    66                                 /*
    67                                  * Test for Transgender flag compatibility. Added in Unicode 13.
    68                                  *
    69                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    70                                  * the browser doesn't render it correctly (white flag emoji + transgender symbol).
    71                                  */
    72                                 isIdentical = emojiSetsRenderIdentically(
    73                                         '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence
    74                                         '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F'  // separated by a zero-width space
    75                                 );
    76 
    77                                 if ( isIdentical ) {
    78                                         return false;
    79                                 }
    80 
    81                                 /*
    82                                  * Test for UN flag compatibility. This is the least supported of the letter locale flags,
    83                                  * so gives us an easy test for full support.
    84                                  *
    85                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    86                                  * the browser doesn't render it correctly ([U] + [N]).
    87                                  */
    88                                 isIdentical = emojiSetsRenderIdentically(
    89                                         '\uD83C\uDDFA\uD83C\uDDF3',       // as the sequence of two code points
    90                                         '\uD83C\uDDFA\u200B\uD83C\uDDF3'  // as the two code points separated by a zero-width space
    91                                 );
    92 
    93                                 if ( isIdentical ) {
    94                                         return false;
    95                                 }
    96 
    97                                 /*
    98                                  * Test for English flag compatibility. England is a country in the United Kingdom, it
    99                                  * does not have a two letter locale code but rather a five letter sub-division code.
    100                                  *
    101                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    102                                  * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]).
    103                                  */
    104                                 isIdentical = emojiSetsRenderIdentically(
    105                                         // as the flag sequence
    106                                         '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F',
    107                                         // with each code point separated by a zero-width space
    108                                         '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F'
    109                                 );
    110 
    111                                 return ! isIdentical;
    112                         case 'emoji':
    113                                 /*
    114                                  * Why can't we be friends? Everyone can now shake hands in emoji, regardless of skin tone!
    115                                  *
    116                                  * To test for Emoji 14.0 support, try to render a new emoji: Handshake: Light Skin Tone, Dark Skin Tone.
    117                                  *
    118                                  * The Handshake: Light Skin Tone, Dark Skin Tone emoji is a ZWJ sequence combining 🫱 Rightwards Hand,
    119                                  * 🏻 Light Skin Tone, a Zero Width Joiner, 🫲 Leftwards Hand, and 🏿 Dark Skin Tone.
    120                                  *
    121                                  * 0x1FAF1 == Rightwards Hand
    122                                  * 0x1F3FB == Light Skin Tone
    123                                  * 0x200D == Zero-Width Joiner (ZWJ) that links the code points for the new emoji or
    124                                  * 0x200B == Zero-Width Space (ZWS) that is rendered for clients not supporting the new emoji.
    125                                  * 0x1FAF2 == Leftwards Hand
    126                                  * 0x1F3FF == Dark Skin Tone.
    127                                  *
    128                                  * When updating this test for future Emoji releases, ensure that individual emoji that make up the
    129                                  * sequence come from older emoji standards.
    130                                  */
    131                                 isIdentical = emojiSetsRenderIdentically(
    132                                         '\uD83E\uDEF1\uD83C\uDFFB\u200D\uD83E\uDEF2\uD83C\uDFFF', // as the zero-width joiner sequence
    133                                         '\uD83E\uDEF1\uD83C\uDFFB\u200B\uD83E\uDEF2\uD83C\uDFFF'  // separated by a zero-width space
    134                                 );
    135 
    136                                 return ! isIdentical;
    137                 }
    138 
    139                 return false;
     305                var supports = {};
     306                tests.forEach( function ( test ) {
     307                        supports[ test ] = browserSupportsEmoji( context, test );
     308                } );
     309                return supports;
    140310        }
    141311
    … …  
    147317         * @since 4.2.0
    148318         *
    149          * @param {Object} src The url where the script is located.
     319         * @param {string} src The url where the script is located.
     320         *
    150321         * @return {void}
    151322         */
    152323        function addScript( src ) {
    153324                var script = document.createElement( 'script' );
    154 
    155325                script.src = src;
    156                 script.defer = script.type = 'text/javascript';
    157                 document.getElementsByTagName( 'head' )[0].appendChild( script );
    158         }
    159 
    160         tests = Array( 'flag', 'emoji' );
     326                script.defer = true;
     327                document.head.appendChild( script );
     328        }
    161329
    162330        settings.supports = {
    … …  
    165333        };
    166334
    167         /*
    168          * Tests the browser support for flag emojis and other emojis, and adjusts the
    169          * support settings accordingly.
    170          */
    171         for( ii = 0; ii < tests.length; ii++ ) {
    172                 settings.supports[ tests[ ii ] ] = browserSupportsEmoji( tests[ ii ] );
    173 
    174                 settings.supports.everything = settings.supports.everything && settings.supports[ tests[ ii ] ];
    175 
    176                 if ( 'flag' !== tests[ ii ] ) {
    177                         settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && settings.supports[ tests[ ii ] ];
    178                 }
    179         }
    180 
    181         settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && ! settings.supports.flag;
    182 
    183         // Sets DOMReady to false and assigns a ready function to settings.
    184         settings.DOMReady = false;
    185         settings.readyCallback = function() {
    186                 settings.DOMReady = true;
    187         };
    188 
    189         // When the browser can not render everything we need to load a polyfill.
    190         if ( ! settings.supports.everything ) {
    191                 ready = function() {
    192                         settings.readyCallback();
    193                 };
    194 
    195                 /*
    196                  * Cross-browser version of adding a dom ready event.
    197                  */
    198                 if ( document.addEventListener ) {
    199                         document.addEventListener( 'DOMContentLoaded', ready, false );
    200                         window.addEventListener( 'load', ready, false );
    201                 } else {
    202                         window.attachEvent( 'onload', ready );
    203                         document.attachEvent( 'onreadystatechange', function() {
    204                                 if ( 'complete' === document.readyState ) {
    205                                         settings.readyCallback();
     335        // Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired.
     336        var domReadyPromise = new Promise( function ( resolve ) {
     337                document.addEventListener( 'DOMContentLoaded', resolve, {
     338                        once: true
     339                } );
     340        } );
     341
     342        // Obtain the emoji support from the browser, asynchronously when possible.
     343        new Promise( function ( resolve ) {
     344                var supportTests = getSessionSupportTests();
     345                if ( supportTests ) {
     346                        resolve( supportTests );
     347                        return;
     348                }
     349
     350                if ( supportsWorkerOffloading() ) {
     351                        try {
     352                                /*
     353                                 * Note that this string contains the real source code for the
     354                                 * copied functions, _not_ a string representation of them. This
     355                                 * is because it's not possible to transfer a Function across
     356                                 * threads. The lack of quotes is intentional. The function names
     357                                 * are copied to variable names since minification will munge the
     358                                 * function names, thus breaking the ability for the functions to
     359                                 * refer to each other.
     360                                 */
     361                                var workerScript =
     362                                        'var emojiSetsRenderIdentically = ' + emojiSetsRenderIdentically + ';' +
     363                                        'var browserSupportsEmoji = ' + browserSupportsEmoji + ';' +
     364                                        'var testEmojiSupports = ' + testEmojiSupports + ';' +
     365                                        'postMessage(testEmojiSupports(' + JSON.stringify(tests) + '));';
     366                                var blob = new Blob( [ workerScript ], {
     367                                        type: 'text/javascript'
     368                                } );
     369                                var worker = new Worker( URL.createObjectURL( blob ) );
     370                                worker.onmessage = function ( event ) {
     371                                        supportTests = event.data;
     372                                        setSessionSupportTests( supportTests );
     373                                        resolve( supportTests );
     374                                };
     375                                return;
     376                        } catch ( e ) {}
     377                }
     378
     379                supportTests = testEmojiSupports( tests );
     380                setSessionSupportTests( supportTests );
     381                resolve( supportTests );
     382        } )
     383                // Once the browser emoji support has been obtained from the session, finalize the settings.
     384                .then( function ( supportTests ) {
     385                        /*
     386                         * Tests the browser support for flag emojis and other emojis, and adjusts the
     387                         * support settings accordingly.
     388                         */
     389                        for ( var test in supportTests ) {
     390                                settings.supports[ test ] = supportTests[ test ];
     391
     392                                settings.supports.everything =
     393                                        settings.supports.everything && settings.supports[ test ];
     394
     395                                if ( 'flag' !== test ) {
     396                                        settings.supports.everythingExceptFlag =
     397                                                settings.supports.everythingExceptFlag &&
     398                                                settings.supports[ test ];
    206399                                }
    207                         } );
    208                 }
    209 
    210                 src = settings.source || {};
    211 
    212                 if ( src.concatemoji ) {
    213                         addScript( src.concatemoji );
    214                 } else if ( src.wpemoji && src.twemoji ) {
    215                         addScript( src.twemoji );
    216                         addScript( src.wpemoji );
    217                 }
    218         }
    219 
     400                        }
     401
     402                        settings.supports.everythingExceptFlag =
     403                                settings.supports.everythingExceptFlag &&
     404                                ! settings.supports.flag;
     405
     406                        // Sets DOMReady to false and assigns a ready function to settings.
     407                        settings.DOMReady = false;
     408                        settings.readyCallback = function () {
     409                                settings.DOMReady = true;
     410                        };
     411                } )
     412                .then( function () {
     413                        return domReadyPromise;
     414                } )
     415                .then( function () {
     416                        // When the browser can not render everything we need to load a polyfill.
     417                        if ( ! settings.supports.everything ) {
     418                                settings.readyCallback();
     419
     420                                var src = settings.source || {};
     421
     422                                if ( src.concatemoji ) {
     423                                        addScript( src.concatemoji );
     424                                } else if ( src.wpemoji && src.twemoji ) {
     425                                        addScript( src.twemoji );
     426                                        addScript( src.wpemoji );
     427                                }
     428                        }
     429                } );
    220430} )( window, document, window._wpemojiSettings );
Note: See TracChangeset for help on using the changeset viewer.