[wp-trac] [WordPress Trac] #12009: Add support for HTML 5 "async" and "defer" attributes

WordPress Trac noreply at wordpress.org
Fri Jun 16 19:15:19 UTC 2023


#12009: Add support for HTML 5 "async" and "defer" attributes
-------------------------------------------------+-------------------------
 Reporter:  Otto42                               |       Owner:  10upsimon
     Type:  enhancement                          |      Status:  assigned
 Priority:  high                                 |   Milestone:  6.3
Component:  Script Loader                        |     Version:  4.6
 Severity:  normal                               |  Resolution:
 Keywords:  has-patch has-unit-tests 2nd-        |     Focuses:
  opinion                                        |  performance
-------------------------------------------------+-------------------------

Comment (by westonruter):

 Thanks for the quick reply, @azaozz.

 Replying to [comment:125 azaozz]:
 > Currently the inline "after scripts" may run (be placed in the HTML
 source) long after the "main" script. Example: when using script
 concatenation (with load-scripts.php which is always used in production)
 they are outputted after the concatenated script that contains all "main"
 scripts. [...] However the bigger problem here is that this method does
 not match (doesn't have parity with) the way "after" scripts currently
 work (mostly) in production, see above.

 I’m confused because previously
 [https://core.trac.wordpress.org/ticket/12009?replyto=125#comment:86 you
 said] that "support for concatenation would not be a priority and probably
 won't be necessary. This would simplify it a bit.” You’ve also proposed in
 #57548 that we eliminate script concatenation entirely since it is
 obsolete. Nevertheless, `load-scripts.php` isn’t used in production on the
 frontend; it’s only in the admin.

 In any case, Script Loader always skips concatenation for any handle to
 which an inline script is attached. This can be confirmed in
 [https://github.com/WordPress/wordpress-
 develop/blob/bea4307daa21a3f97d159898b0ac676332e0e7de/tests/phpunit/tests/dependencies/scripts.php#L575-L601
 the existing unit tests].

 > Seems that to match this behaviour the "after" scripts for delayed main
 scripts would have to be executed on the DOMContentLoaded event, or
 another appropriate event, that ensures that all delayed scripts have
 finished executing. This could be done from PHP by wrapping each "after"
 script in a lambda JS function that would execute at the appropriate time.
 ¶  Looking further, using this method would guarantee that the "after"
 scripts will be executed after the delayed main scripts in all cases,
 regardless of when and where the inline scripts appear in the HTML.
 However as the Script Loader API is a JavaScript API (not PHP), adding
 that lambda function may be considered "too much hand-holding", as this is
 a very well known, very basic pattern in JS. ¶ As mentioned above, support
 for "after" scripts can either be revised to achieve parity with current
 concatenated scripts, or completely removed and left for themes/plugins to
 handle it in JS. If removed there should be enough documentation
 explaining why, and how to add inline scripts that would execute after the
 main script has finished executing. Seems the most appropriate way would
 be to run any "after" scripts on the DOMContentLoaded event.

 We actually considered relying on the `DOMContentLoaded` event to run the
 after inline scripts, but the problem with this is that existing inline
 after scripts may be attempting to define something in the global scope
 without explicitly setting it on the `window`. For example:

 {{{#!js
 wp_add_inline_script( 'foo', 'var fooHasLoaded = true;' );
 }}}

 Such an after inline script would break if it were automatically wrapped
 in an event handler callback. So instead our approach of dynamically
 inserting the `script[type="text/plain"]` after scripts is fully backwards
 compatible since anything defined in the function will go into the global
 scope.

 > Even more, these inline scripts can be outputted directly by the plugins
 on different PHP "actions" or with different hook priority. There is
 actually no need for plugins to add any "before" or "after" inline scripts
 in Script Loader at all as long as the JS is written properly. (I wouldn't
 call this "best practice", but it is possible.)

 Right, this would not be a best practice as it disconnects the inline
 scripts from the scripts they relate to, making dequeuing them difficult
 or impossible.

 > > 2. The implementation method in the current PR for delaying inline
 scripts is unsafe
 > Wouldn't call it "unsafe", just "unneeded" and perhaps a bit "weird" :)

 It doesn’t seem particularly weird when compared with how external scripts
 often get dynamically inserted into the page (i.e. lazy-loading JS). For
 example, the [https://github.com/WordPress/wordpress-
 develop/blob/ba9a2f8b/src/js/_enqueues/lib/emoji-loader.js#L152-L158
 emoji-loader does this]:

 {{{#!js
 function addScript( src ) {
         var script = document.createElement( 'script' );

         script.src = src;
         script.defer = script.type = 'text/javascript';
         document.getElementsByTagName( 'head' )[0].appendChild( script );
 }
 }}}

 This can also be seen
 [https://github.com/mediaelement/mediaelement/blob/0dec971/src/js/utils/dom.js#L12-L27
 in MediaElement.js] and
 [https://github.com/WordPress/gutenberg/blob/1d9c4fa/packages/block-
 editor/src/components/iframe/index.js#L89-L102 in Gutenberg]. The only
 difference is that we’re doing this for inline scripts instead of external
 ones.

 > The question here is: do the browsers handle <script> tags exactly in
 the same way and with the same security restrictions like <script type
 ="something-else"> tags? Think that wasn't the case some time ago, not
 sure now. You're right, the use of nonces seems to remove many security
 concerns.

 Good to hear!

 > > 3. Supporting async and defer could conflict with future dynamic
 module imports
 > Thanks for looking at this! Yes, seems that dynamic module loading will
 be a browser controlled "pure JS" thing, completely "outside" of Script
 Loader.

 Agreed. Nevertheless, I think Script Loader is still useful to enqueue
 module scripts, even though the dependencies array may be empty. Even so,
 the dependencies could end up being useful still as they could perhaps be
 added to a `script[type=importmap]`.

 > > ...where data from the server is being supplied to an inline script in
 an after position
 > Yea, this is generally "doing_it_wrong" as long as Script Loader is
 concerned. See examples from all previous inline scripts that supply data
 (like the old translated strings). While it is possible to do I'd consider
 it "bad practice" for any JS that is handled by Script Loader.

 The problem with the examples in Script Loader that are using before
 inline scripts is that they require the use of a global variable. There
 are a [https://github.com/search?q=repo%3AWordPress%2Fwordpress-
 develop%20add_inline_script&type=code couple dozen examples] of after
 inline scripts being used in core which allow data to be passed directly
 to the script without involving a global variable. This is beneficial for
 a few reasons, including: (1) reduces memory usage, (2) prevents pollution
 of global scope, (3) allows existing libraries to be re-used without
 having to work in a shim to make them look for global variables for
 configuration. So to me it seems like quite a good practice. Some examples
 of passing data to a script via an after inline script include
 [https://github.com/WordPress/wordpress-develop/blob/3e2121c/src/wp-
 includes/block-editor.php#L642-L649 block-editor.php],
 [https://github.com/WordPress/wordpress-develop/blob/3e2121c/src/wp-admin
 /site-editor.php#L110-L114 site-editor.php], and
 [https://github.com/WordPress/wordpress-develop/blob/3e2121c/src/wp-
 includes/general-template.php#L3909 general-template.php].

 > Can make the core scripts "non-portable" for no good reason.

 How do you mean?

 > Don't think that would ever be possible because of the edge cases. It
 would open a huuuge can of worms :) ¶ I'm actually thinking the current PR
 may need to include a way to prevent (and add "doing_it_wrong") cases
 where a plugin or theme attempts to re-register any core script with async
 or defer. Unfortunately seems this functionality could be used only for
 newly added core scripts as there's no way to ensure 100% back-compat for
 the loading and execution order.  ¶ Do you think a method to detect when a
 plugin or a theme "tweaks" a core script to add defer or async is worth
 adding? Can probably see if that becomes common and then add it?

 Actually, being able to transition applicable core scripts to use a
 delayed strategy is specifically what we want to explore once this gets
 merged (e.g. `comment-reply`). We’ve specifically developed our
 implementation in a way that retains compatibility between core delayed
 scripts and theme/plugin blocking scripts. In particular, if a core script
 adopts a delayed strategy but a theme/plugin is enqueueing a blocking
 script that depends on the core-delayed script, then our implementation
 forces the core script to revert to being blocking. So we have implemented
 a back-compat method to maintain the loading and execution order,
 including for before/after inline scripts.

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/12009#comment:126>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list