Make WordPress Core

Opened 7 years ago

Last modified 10 days ago

#12009 reopened enhancement

Add support for HTML 5 "async" and "defer" attributes

Reported by: Otto42 Owned by: azaozz
Milestone: Awaiting Review Priority: normal
Severity: normal Version: 4.6
Component: Script Loader Keywords:
Focuses: Cc:

Description (last modified by scribu)

HTML5 supports async and defer attributes on script tags: http://www.w3.org/TR/html5/semantics.html#attr-script-async

Basic usage of these:

  • "async" scripts get executed asyncronously, as soon as they're loaded. This lets them run without slowing down the page parsing (normally, all page processing stops while the javascript code is executing).
  • "defer" scripts get deferred from running until page processing is complete. Sorta like jQuery(document).ready() does, except without pre-definitions. Faster, in other words, since it's built into the browser.

Correct usage would dictate that "libraries" like jQuery and such would get the async attribute, while bits of code that use the current DOM would get deferred. The defer bit is basically optional though, since most all code that exists uses something like jQuery(document).ready() already, when it's necessary, and so there's not a lot of benefit there.

The just released Firefox 3.6 supports the async attributes, so you can do testing with these immediately. I've noticed a speedup on the wp-admin side of things by using it, but I have not measured this and cannot be sure I'm not imagining it. Still, it does seem like it makes the page appear faster.

Attachments (4)

defer and async.patch (2.5 KB) - added by scep 5 years ago.
async_defer_scripts.patch (6.4 KB) - added by wpnook 10 months ago.
Adds defer/async attribute option for wp_enqueue_scripts
async_defer_scripts_new.patch (3.4 KB) - added by wpnook 10 months ago.
Removes code unrelated to this ticket
async_defer_scripts_new.2.patch (3.4 KB) - added by wpnook 10 months ago.
Removes leftover print_r

Download all attachments as: .zip

Change History (38)

#1 @scribu
7 years ago

  • Milestone changed from Unassigned to 3.0

We could add a new parameter to the script loader: load_type, with one of the following values:

  • (normal loading)
  • 'defer'
  • 'async'

#2 @nacin
7 years ago

  • Milestone changed from 3.0 to Future Release

This seems like a good idea to eventually do, but no patch or traction yet. Future release for now.

#3 @ptahdunbar
6 years ago

  • Cc trac@… added
  • Keywords needs-patch added

#4 @azaozz
6 years ago

This sounds good but to implement it and actually use it we would need at least the majority of the browsers in use to support it properly (as per the HTML5 specs). This seems to be "very far in the future" for now.

#5 follow-up: @scribu
5 years ago

  • Description modified (diff)

Alternatively, we could just add a filter to wp_print_scripts(), allowing users to add attributes when they need to.

As always, users find weird workarounds when the API doesn't oblige. For example, using the clean_url filter:


#6 @toscho
5 years ago

  • Cc info@… added

#7 @scep
5 years ago

  • Cc scep added
  • Keywords has-patch added; needs-patch removed

I was also looking for this feature, saw that it did not exist so i took a shot at it writing it in, I attached a patch for what I did.

#8 @scep
5 years ago

  • Keywords needs-testing added

#9 in reply to: ↑ 5 @azaozz
5 years ago

Replying to scribu:

As always, users find weird workarounds when the API doesn't oblige...

Agreed. However I'm still feeling a bit "uneasy" about adding support for a feature that is not supported by more than half of the browsers currently in use: http://en.wikipedia.org/wiki/Usage_share_of_web_browsers.

#10 follow-up: @toscho
5 years ago

These attributes are backwards compatible. I don’t think supporting them would do any harm.

But I don’t like more arguments for wp_register_script(). We have already four, that’s more than enough. How about a very simple extra string in WP_Scripts::do_item()?

$src = esc_url( apply_filters( 'script_loader_src', $src, $handle ) );
$extra = apply_filters( 'script_extra', '', $src, $handle );

if ( $this->do_concat )
	$this->print_html .= "<script type='text/javascript' src='$src' $extra></script>\n";
	echo "<script type='text/javascript' src='$src' $extra></script>\n";

#11 @azaozz
5 years ago

Yes, another filter would do the trick but has many drawbacks. If we decide to use these attributes in core, there will be no good way to add them: it will interfere with what plugins are adding (or removing) in all cases.

#12 in reply to: ↑ 10 ; follow-up: @scep
5 years ago

Replying to toscho:

These attributes are backwards compatible. I don’t think supporting them would do any harm.

But I don’t like more arguments for wp_register_script(). We have already four, that’s more than enough. How about a very simple extra string in WP_Scripts::do_item()?

I agree that since they are backwards compatible they should be supported.

I also agree that my patch adds a lot of extra arguments for the register and enqueue functions. I like you filter idea but it seems like it would lead to having a lot of code in the return function to identify which scripts should have extra info attached to them.

#13 in reply to: ↑ 12 @scribu
5 years ago

Replying to scep:

I also agree that my patch adds a lot of extra arguments for the register and enqueue functions. I like you filter idea but it seems like it would lead to having a lot of code in the return function to identify which scripts should have extra info attached to them.

Every script has a unique ID which it can be identified by (the first parameter to wp_enqueue_script()).

#14 @wycks
5 years ago

Here is the current Compatibility (Aug 2012):

Firefox: yes
Chrome: yes
Safari: yes
Andriod: yes
Blackberry: yes
IE version 10: yes

IE 6, 7, 8, 9: no

Opera: no (and no future plans)


ps. For reference the much used google analytic code uses async by setting var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;

#15 @georgestephanis
5 years ago

Leaving a comment to remind myself to come back and write a patch for this later. This is a great example of graceful degradation that probably should be supported.

#16 @Otto42
5 years ago

I like the patch, but instead of adding new params to the function calls, lets change the $in_footer parameter to be allowed as an array of attributes.

Basically, instead of $in_footer = false, have $attr = false.

For backward compatibility, you could do if $attr == true, then $attr = array('in_footer'=>true).

Then, you can have $attr = an array of the various settings, including potential future ones, allowing you to define whether it's in the footer or not, defer or not, async or not, etc.

Version 0, edited 5 years ago by Otto42 (next)

#17 @scribu
5 years ago

Or, we could just throw our hands in the air: add a 'script_loader_tag' and let plugins add whatever attributes they see fit: #13592

#18 @ryanve
5 years ago

The ideal way to handle script/style attributes is via #22249 because it is future-proof for any new attributes that are invented.

The 'script_loader_tag' should be added for consistency with the already existing 'style_loader_tag' to allow for customizations that go beyond attributes, such as wrapping in IE conditionals (#16024) or adding embedded scripts. (#13592 and #22245)

Last edited 5 years ago by ryanve (previous) (diff)

#19 @kevinlangleyjr
4 years ago

  • Cc kevinlangleyjr added

#20 @lkraav
4 years ago

  • Cc leho@… added

#21 @nacin
4 years ago

The 'script_loader_tag' should be added

I agree.

#22 @pothi
3 years ago

  • Cc pothi added

#23 @nacin
3 years ago

  • Component changed from JavaScript to Script Loader

#24 @webaware
3 years ago

A further complication: CloudFlare Rocketscript. This is a service that compresses and bundles scripts from your website into a single download. It does this by replacing your script elements with ones that look like this:

<script type='text/rocketscript' data-rocketsrc='http://example.com/path/to/src.js?ver=1.0'></script>

After the page has loaded, Rocketscript collects those elements all together and loads / executes the scripts. Which means scripts that don't like being loaded asynchronously break of course.

To disable Rocketscript for specific scripts, you can add an attribute data-cfasync="false" as per this support note:


However: it only works if the attribute comes before the src attribute.

So I like Toscho's solution, but with $extra added into the script element before the src attribute, not after it.

FYI: this is how I solved the problem for a plugin user, without such extra attribute support; nasty but effective. I'd love to offer a better solution :)


#25 @SergeyBiryukov
3 years ago

#27672 was marked as a duplicate.

#26 @grapplerulrich
2 years ago

Could this be closed as the filter script_loader_tag has been added? #13592

#27 @wonderboymusic
2 years ago

  • Resolution set to fixed
  • Status changed from new to closed

#28 @ocean90
2 years ago

  • Milestone Future Release deleted
  • Resolution changed from fixed to worksforme

#29 @wpnook
10 months ago

  • Resolution worksforme deleted
  • Status changed from closed to reopened
  • Version set to trunk

Is there any interest in reviving this topic? I was going to create a ticket, but found this ticket, so I'll submit my patch here. Browser support seemed to be a big part of the debate previously and shouldn't be a concern now.

The lack of options for using the async and/or defer attributes with script elements is a bit of a pain in WordPress, which has resulted in some users coming up with dirty/hackish ways of doing so, such as using the clean_url filter to add the attribute(s).

The script_loader_tag solves this to a degree, but it would be better to specify this option at the time of loading the script (the filter has other uses as well).

This patch adds two new arguments for {{{wp_enqueue_script}} and outputs the results in the printed <script> tag, and both default to false.

Using both tags simultaneously may be beneficial for legacy browser support, and some third-party documentation (Google Maps JS API for example) recommends using the attributes together.

10 months ago

Adds defer/async attribute option for wp_enqueue_scripts

10 months ago

Removes code unrelated to this ticket

#30 @SergeyBiryukov
10 months ago

  • Milestone set to Awaiting Review

10 months ago

Removes leftover print_r

#31 @azaozz
10 months ago

  • Keywords has-patch needs-testing removed

FTR async and defer do pretty much the same thing: begin to download the script in parallel immediately. The difference is that each async script executes after it has finished downloading. It’s likely that async scripts are not executed in the order in which they occur in the page. The defer scripts are guaranteed to be executed in the order they occur in the page (https://webkit.org/blog/1395/running-scripts-in-webkit/).

Then logically:

  • async and defer make little sense when scripts are concatenated.
  • async scripts cannot be used as dependencies.
  • defer scripts can be used as dependencies only if all dependent scripts also have the defer attribute.
  • Neither async nor defer scripts can have any inline scripts.

In that terms the above patch is a good start but is missing a lot. Looks like this will be quite complex to implement in WP_Scripts. Perhaps it can be made a bit simpler by supporting these attributes only for third party scripts, etc.

#32 @mor10
9 months ago

FWIW I support adding the option of async / defer to a regular enqueue call at the individual script level. This should not be an automated process but a developer decision on a script-by-script basis to ensure proper hierarchy etc. Requiring a filter seems excessive considering all we're talking about is two possible properties without variables or values. As HTTP/2 rolls out, this is becoming a more relevant issue because script concatenation is an anti-pattern in the new multiplexed reality.

#33 @seancjones
2 months ago

I think this is an important issue to resolve. As of now, Google Page Speed Insights failing for a lot of WordPress installs can almost exclusively be blamed on render-blocking JavaScript. Fixing that increases speed by wide margins.

Plugin developers should have a way of deferring page loads. There are clear pitfalls to avoid - like people's websites breaking.

I'm going to play devil's advocate and argue why a filter might be better.

A filter allows a user with a code sample to defer scripts, or remove scripts if they appear to break their current setup. Plugin developers can send an array of script handles to defer via a plugin, rather than rewrite their code with a new parameter.

If a user wanted to disable all deferring:

add_filter ( 'wp_defer_scripts', function($wp_defer) {
  return array();
}, 900, 2 );

add_filter ( 'wp_async_scripts', function($wp_async) {
  return array();
}, 900, 2 );

For someone to enable all hooks:

add_filter ( 'wp_async_scripts', function($wp_async) {
    global $wp_scripts;

    return $wp_scripts->queue;
} );

This would also make it far easier for plugin developers to patch their plugins.

#34 @georgestephanis
10 days ago

Personally I'd prefer not to have the defer / async as arguments in the register/enqueue script functions, but require a wp_script_add_data() call afterwards to flag it on -- the same way we do for rtl support.

Quick tangential gist I threw together for testing of something related, if anyone wants to play with deferring now:


Note: See TracTickets for help on using tickets.