Bug 20710 - WebKit should support defer and async on script elements
Summary: WebKit should support defer and async on script elements
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Page Loading (show other bugs)
Version: 528+ (Nightly build)
Hardware: All All
: P2 Enhancement
Assignee: Tony Gentilcore
URL: http://www.websiteoptimization.com/sp...
Keywords:
Depends on: 39026 40934 43391 43633 45310
Blocks:
  Show dependency treegraph
 
Reported: 2008-09-07 18:25 PDT by Anthony Ricaud
Modified: 2010-09-09 21:32 PDT (History)
25 users (show)

See Also:


Attachments
Patch (23.91 KB, patch)
2010-05-18 21:31 PDT, Tony Gentilcore
no flags Details | Formatted Diff | Diff
Patch (23.49 KB, patch)
2010-05-19 14:38 PDT, Tony Gentilcore
no flags Details | Formatted Diff | Diff
Patch (23.69 KB, patch)
2010-05-19 17:15 PDT, Tony Gentilcore
no flags Details | Formatted Diff | Diff
Patch (23.68 KB, patch)
2010-05-19 18:02 PDT, Tony Gentilcore
no flags Details | Formatted Diff | Diff
Patch (26.87 KB, patch)
2010-06-01 10:42 PDT, Tony Gentilcore
abarth: review-
Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Anthony Ricaud 2008-09-07 18:25:28 PDT
IE already support defer and Firefox will support it (https://bugzilla.mozilla.org/show_bug.cgi?id=28293)

Preloading is already making things better but rendering is still blocked while parsing and executing scripts.

http://www.w3.org/TR/html5/tabular.html#script
Comment 1 Antti Koivisto 2008-09-07 21:48:14 PDT
While this won't help complete load time (since we are already able to load scripts in parallel) this is probably worth implementing for the early display benefit.

Does anyone else support async or just defer?
Comment 2 Robert Blaut 2008-09-08 02:42:34 PDT
Test case for "defer" atribute:

Result of the test with the latest Gecko nightly with defer support implemented:

Inline Head
External Head
Inline Body
External Body
Inline Head Deferred
External Head Deferred
Inline Body Deferred
External Body Deferred

Actual WebKit result:

Inline Head Deferred
Inline Head
External Head Deferred
External Head
Inline Body Deferred
Inline Body
External Body Deferred
External Body
Comment 3 Robert Blaut 2008-09-08 02:43:17 PDT
Here you are the link to the above mentioned test: http://www.websiteoptimization.com/speed/tweak/defer/test/
Comment 4 Michael Dayah 2009-01-05 10:22:02 PST
Firefox finished this. It's in the 3.1 betas now. However, they fire DOMContentLoaded before executing deferred scripts, and they may change their mind on that. Seems like that'd be something worth coordinating. They also don't appear to expose any programmatic way of detecting whether defer is supported, something we also might want to coordinate on doing.
Comment 5 Michael Dayah 2009-03-25 09:16:51 PDT
Firefox decided to fire DCL after deferred scripts to faciliate the load pattern mentioned in this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=28293
Comment 6 Peter Kasting 2009-04-28 11:13:43 PDT
Note, the w3 link at top is now invalid.  Here's a link to the current spec on this: http://www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#script
Comment 7 Michael Dayah 2009-09-05 09:40:57 PDT
Internet Explorer before version 8 have a strange defer order. They execute deferred inline scripts before deferred external scripts. IE8 and Firefox 3.5, however, consistently load in the order described in comment #2.

Firefox also has moved the DOMContentLoaded event to fire after deferred scripts are loaded. This appears to be final and a closed issue. I don't anticipate any further changes in their defer implementation.

External dependencies blocking page load is a problem we now have the tools to deal with thanks to defer covering 85% of the web. Defer is also part of the XHTML and HTML 4 specifications. With Webkit's implementation of defer, this will go over 90% and start becoming a mainstream pattern. I suggest this be moved up in priority for these reasons.
Comment 8 kangax 2009-11-22 20:51:10 PST
> They also don't appear to expose any programmatic way of 
> detecting whether defer is supported, something we also 
> might want to coordinate on doing.

The inference based on property existence works quite well, actually:

'defer' in document.createElement('script'); // true in 3.5+
'async' in document.createElement('script'); // true in 3.6+

// etc.
Comment 9 Tony Gentilcore 2010-05-18 21:31:47 PDT
Created attachment 56451 [details]
Patch
Comment 10 Tony Gentilcore 2010-05-18 21:35:41 PDT
Here's a proposed patch. It supports defer and async on only external scripts and loaded them prior to OnDOMContentLoaded.

I'm slightly worried that although the spec says async and defer must only be supported for external scripts, since Moz and IE support defer on inline scripts, we might have to do that for compat. What do others think?
Comment 11 Brian Kuhn 2010-05-18 22:36:17 PDT
I can't find the exact language in the spec, but Ian claims to have modified it to say that async scripts should not block DOMContentLoaded.  I'd prefer they also didn't block window.onload, but I'll take what I can get...

http://canvex.lazyilluminati.com/misc/cgi/issues.cgi/message/%3C4BA14D55.5060600@souders.org%3E

"On 3/16/2010 5:05 PM, Ian Hickson wrote:
I've changed the spec to fire 'DOMContentLoaded' without waiting for the
async scripts, so that if you need this you can just listen for that event
instead of 'load'. 'load' still waits for all scripts. 'DOMContentLoaded'
still waits for deferred scripts. As far as I can tell this handles all
the above (still makes sense, still consistent with the way other 'load'
events work, but still lets you do things without waiting)."
Comment 12 WebKit Review Bot 2010-05-18 23:54:17 PDT
Attachment 56451 [details] did not build on gtk:
Build output: http://webkit-commit-queue.appspot.com/results/2315301
Comment 13 Tony Gentilcore 2010-05-19 14:38:52 PDT
Created attachment 56522 [details]
Patch
Comment 14 Tony Gentilcore 2010-05-19 14:48:04 PDT
Brian, you were right. Async should not block DOMContentLoaded. Here's the relevant part of the spec:
http://dev.w3.org/html5/spec/Overview.html#delay-the-load-event

As further validation, I've compiled this table lists the behavior of current browsers and specs as it related to async/defer.

           defer inl.  defer ext.  async  block DCL   block load
           ----------  ----------  -----  ----------  ----------
HTML4 spec      Y          Y         N      undef       undef
HTML5 spec      N          Y         Y    only defer      Y
IE 8            Y          Y         N       N/A          Y
Opera 10.5      N          N         N       N/A         N/A
Firefox 3.6     N          Y         Y        N           Y
Curr WebKit     N          N         N       N/A         N/A
New WebKit      N          Y         Y    only defer      Y

This patch matches the HTML5 spec and only differs from FF3.6 in that we block DCL for defer. So we shouldn't have any web compat problems that aren't in FF3.6.

I've updated the patch as describe above (and it should also now fix the gtk/qt builds).

To reviewer: I could break defer and async into separate patches if you prefer, but I thought it important to see the whole thing first.
Comment 15 WebKit Review Bot 2010-05-19 16:13:14 PDT
Attachment 56522 [details] did not build on gtk:
Build output: http://webkit-commit-queue.appspot.com/results/2291318
Comment 16 Tony Gentilcore 2010-05-19 17:15:35 PDT
Created attachment 56538 [details]
Patch
Comment 17 Tony Gentilcore 2010-05-19 18:02:37 PDT
Created attachment 56542 [details]
Patch
Comment 18 Tony Gentilcore 2010-05-24 15:27:26 PDT
Adam/Eric, I noticed you guys are landing the HTML5 parser. Thought I should check with you early on this patch to make sure it fits with those plans.
Comment 19 Tony Gentilcore 2010-05-26 13:17:21 PDT
Darin/Maciej, this seems like the sort of thing you guys might be interested in.
Comment 20 Eric Seidel (no email) 2010-05-26 13:29:59 PDT
I'll review this later today.  I just got through re-writing the Script architecture for HTML5 as part of bug 39716.
Comment 21 Eric Seidel (no email) 2010-05-26 18:18:09 PDT
It is unfortunate to put this code on Document since it's only needed during parsing.  Tony and I talked in #webkit he said he'd look at the possibility of re-using HTML5ScriptRunner to do this once a first-pass implementation lands as part of bug 39716 (likely tonight).

I'm not sure we would actually be able to make HTML5ScriptRunner work with the old Tokenizer, but there is also the question of whether we even want to bother fixing this for the soon-to-be-old parser path.  I'm not sure when we'll have the HTML5 parser system ready for prime time though so it may not make sense to block this work either.

Regardless, the tests he wrote for this are super useful.
Comment 22 Tony Gentilcore 2010-06-01 10:42:15 PDT
Created attachment 57565 [details]
Patch
Comment 23 Tony Gentilcore 2010-06-01 10:45:22 PDT
(In reply to comment #21)
> It is unfortunate to put this code on Document since it's only needed during parsing.  Tony and I talked in #webkit he said he'd look at the possibility of re-using HTML5ScriptRunner to do this once a first-pass implementation lands as part of bug 39716 (likely tonight).
> 
> I'm not sure we would actually be able to make HTML5ScriptRunner work with the old Tokenizer, but there is also the question of whether we even want to bother fixing this for the soon-to-be-old parser path.  I'm not sure when we'll have the HTML5 parser system ready for prime time though so it may not make sense to block this work either.
> 
> Regardless, the tests he wrote for this are super useful.

I've update the patch to work with the HTML5ScriptRunner. It worked beautifully with defer. However, async is a little different since it needs to block the load event but not parsing or the DCL event. Since async scripts may outlive the parser, the two options I see for tracking pending async scripts are either the Document or the FrameLoader. This patch uses the document for now. Let me know if you think it would be cleaner to move to FrameLoader or if you have an idea for a third option.

Also, I don't have any expectations along with my new layout tests. What is the right procedure to add the expectations?
Comment 24 Adam Barth 2010-06-01 11:11:39 PDT
Comment on attachment 57565 [details]
Patch

We'll want Eric to do the final review, but here are some comments for now.

LayoutTests/http/tests/local/resources/async-script.js:1
 +  window.status_ += " async ";
The trailing underscore is a Google-ism.  It's probably ok for tests to use a variety of styles, but I just mention it so you're aware.

LayoutTests/ChangeLog:5
 +          Added tests for defer and async script attributes.
Might be worth noting there that these tests are supposed to pass in the HTML5 parser but fail in the old parser.

LayoutTests/ChangeLog:14
 +          * http/tests/local/script-defer.html: Added.
If you run-webkit-tests, it should generate the missing expectation files.  You can specify tests to run on the command line if you don't want to run the whole suite.

LayoutTests/http/tests/local/resources/slow-async-script.cgi:4
 +  sleep(0.5);
Boo.  This will make the test somewhat slow and possibly flaky...  Not sure how to make it better though...

LayoutTests/http/tests/local/script-async.html:28
 +          layoutTestController.notifyDone();
If you're going to call notifyDone in the onload handler anyway, you don't need to waitUntilDone().  The default end of the test is after the onload handler finishes.

LayoutTests/http/tests/local/script-async.html:32
 +  <script src="http://127.0.0.1:8000/local/resources/slow-async-script.cgi" async="ASYNC"></script>
Do we want to test some syntactic variations of these attributes?

LayoutTests/http/tests/local/script-defer.html:20
 +      if (window.status_ != expected)
If there's just one expected result, we can just dump out the result and manage the expectations in the -expected.txt file, but it doesn't really matter.

LayoutTests/ChangeLog:12
 +          * http/tests/local/resources/slow-defer-script.cgi: Added.
Why are these tests in the "local" directory?  They don't seem to interact with the file system (which is why things go in local).

WebCore/ChangeLog:9
 +          the legacy parser.
Would be nice to have more information in the ChangeLog for future developers trying to understand why you did what you did.

WebCore/dom/ScriptElement.cpp:314
 +      return !m_scriptElement->sourceAttributeValue().isEmpty() && m_scriptElement->asyncAttributeValue();
Can you add a link to the spec where this is defined?  It's not obvious to me that this is the correct resolution when an inline script has an async attribute.

WebCore/html/HTML5ScriptRunner.cpp:148
 +      m_scriptsToExecuteAfterParsing.removeFirst();
There isn't a takeFirst method that does both of these?  Maybe we should add one?

WebCore/html/HTML5ScriptRunner.cpp:151
 +          return executeScriptsWaitingForParsing();
Will this recursion explode if there are thousands of these scripts?  Maybe a looping construct is better?  Can running the script cause re-entrancy here we need to worry about?

WebCore/dom/ScriptElement.cpp:320
 +      return !m_scriptElement->sourceAttributeValue().isEmpty() && !m_scriptElement->asyncAttributeValue() && m_scriptElement->deferAttributeValue();
We need a test for this logic.

WebCore/dom/ScriptElement.cpp:314
 +      return !m_scriptElement->sourceAttributeValue().isEmpty() && m_scriptElement->asyncAttributeValue();
Also, we need a test for this logic.

WebCore/html/HTML5ScriptRunner.cpp:147
 +      m_parsingBlockingScript = m_scriptsToExecuteAfterParsing.first();
How can a script be blocking parsing if we're done parsing?

WebCore/html/HTML5ScriptRunner.cpp:238
 +      PendingScript pendingScript = PendingScript();
No need to call the default constructor explicitly.

WebCore/html/HTML5ScriptRunner.cpp:239
 +      requestScript(&pendingScript, scriptElement);
If pendingScript is a value type, we should pass it by non-const reference.  If pendingScript is an output parameter, we should lass it last.

WebCore/html/HTML5ScriptRunner.cpp:253
 +          // Asynchronous scripts are not handled by the script runner because they do not block parsing.
But deferred scripts do block parsing?

WebCore/html/HTML5Tokenizer.cpp:91
 +      if (shouldContinueParsing)
Do we need a not here?  If we should continue parsing, then we don't want to finish the tree builder.  If this branch goes the other way, who will call finish on the tree builder?

WebCore/html/HTMLScriptElement.cpp:164
 +      return deferAttributeValue();
Why add this virtual method call?  Maybe have the virtual one call the non-virtual one?

WebCore/html/HTMLScriptElement.cpp:159
 +      setAttribute(asyncAttr, async ? "" : 0);
Please add a test for this.
Comment 25 Eric Seidel (no email) 2010-06-01 11:23:21 PDT
I recommend we split this into two patches.

1.  Which just adds the "async" and "defer" parsing and idl changes.  We add some tests which tests parting variations of the modes and which one trumps which (assuming that's exposed to JS at the property level).

2.  The patch which actually does the scheduling.  The scheduling parts are slightly tricky since HTML5ScriptRunner is still changing.

It will be simple to get patch #1 reviewed by anyone.  Patch #2 you and I can work through over the next couple days?

I think it's OK to put async scripts on the document.  We could also make the ScriptRunner out-live the tokenizer (be owned by the Document or something else instead).  But I think it's fine to put the logic on the document.  We could make a new class to handle holding the async scripts, unsure.

I'm not when exactly async scripts get executed.  The spec says:
If the async attribute is present, then the script will be executed asynchronously, as soon as it is available.

That seems to me that it should be executed as soon as stylesheets are ready but possibly before parsing is completed, no?  I guess since they may execute after the load event they can't be executed by the ScriptRunner exclusively (since I think the tokenizer is deleted before the load event).  But it seems like async scripts will have trouble with our current document.write implementation since it (wrongly) assumes that the only thing executing scripts during parsing is the HTML5ScriptRunner.

I think this is a good change, but we should do it in smaller pieces.  Then there is less possible discussion and risk with each piece.
Comment 26 Tony Gentilcore 2010-06-01 11:32:11 PDT
(In reply to comment #25)
> I recommend we split this into two patches.

Good idea.

> 
> 1.  Which just adds the "async" and "defer" parsing and idl changes.  We add some tests which tests parting variations of the modes and which one trumps which (assuming that's exposed to JS at the property level).

Sounds good, I'll mail this soon with tests in fast/dom (let me know if they should live somewhere else).

> 
> 2.  The patch which actually does the scheduling.  The scheduling parts are slightly tricky since HTML5ScriptRunner is still changing.
> 
> It will be simple to get patch #1 reviewed by anyone.  Patch #2 you and I can work through over the next couple days?
> 
> I think it's OK to put async scripts on the document.  We could also make the ScriptRunner out-live the tokenizer (be owned by the Document or something else instead).  But I think it's fine to put the logic on the document.  We could make a new class to handle holding the async scripts, unsure.

I think the cleanest approach would be to make ScriptRunner live longer, but that is a harder change and might be easier once the old parser is out of the picture. Perhaps I should add a FIXME comment in the Document mentioning that.

> 
> I'm not when exactly async scripts get executed.  The spec says:
> If the async attribute is present, then the script will be executed asynchronously, as soon as it is available.

That is defined here:
http://dev.w3.org/html5/spec/Overview.html#the-end

Unfortunately, we don't have one area that implements "the end", that is spread across parser/tokenizer, document and frameloader.

> 
> That seems to me that it should be executed as soon as stylesheets are ready but possibly before parsing is completed, no?  I guess since they may execute after the load event they can't be executed by the ScriptRunner exclusively (since I think the tokenizer is deleted before the load event).  But it seems like async scripts will have trouble with our current document.write implementation since it (wrongly) assumes that the only thing executing scripts during parsing is the HTML5ScriptRunner.
> 
> I think this is a good change, but we should do it in smaller pieces.  Then there is less possible discussion and risk with each piece.

Adam, I'll address your comments in #2 after #1 is landed.