Add SPI to defer running async script until after document load
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Jul 2018 17:08:46 +0000 (17:08 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Jul 2018 17:08:46 +0000 (17:08 +0000)
https://bugs.webkit.org/show_bug.cgi?id=187748
<rdar://problem/42317378>

Reviewed by Ryosuke Niwa and Tim Horton.

Source/WebCore:

On watchOS, we currently observe that time-consuming async scripts can block the first paint of Reader, leaving
the user with a blank screen for tens of seconds. One way to mitigate this is to defer async script execution
until after document load (i.e. the same timing as DOMContentLoaded).

This patch introduces an SPI configuration allowing internal clients to defer execution of asynchronous script
until after document load; this, in combination with the parser yielding token introduced in r233891, allows
Safari on watchOS to avoid being blocked on slow script execution before the first paint of the Reader page on
most article-like pages. See below for more details.

Test: RunScriptAfterDocumentLoad.ExecutionOrderOfScriptsInDocument

* dom/Document.cpp:
(WebCore::Document::shouldDeferAsynchronousScriptsUntilParsingFinishes const):
(WebCore::Document::finishedParsing):

Notify ScriptRunner when the Document has finished parsing, and is about to fire DOMContentLoaded.

* dom/Document.h:
* dom/ScriptRunner.cpp:
(WebCore::ScriptRunner::documentFinishedParsing):

When the document is finished parsing, kick off the script execution timer if needed to run any async script
that has been deferred.

(WebCore::ScriptRunner::notifyFinished):
(WebCore::ScriptRunner::timerFired):

Instead of always taking from the list of async scripts to execute, check our document to see whether we should
defer this until after document load. If so, ignore `m_scriptsToExecuteSoon`.

* dom/ScriptRunner.h:
* page/Settings.yaml:

Add a WebCore setting for this behavior.

Source/WebKit:

Add plumbing for a new ShouldDeferAsynchronousScriptsUntilAfterDocumentLoad configuration that determines
whether async script execution should be deferred until document load (i.e. DOMContentLoaded). This
configuration defaults to NO on all platforms. See WebCore ChangeLog for more detail.

* Shared/WebPreferences.yaml:
* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _initializeWithConfiguration:]):
* UIProcess/API/Cocoa/WKWebViewConfiguration.mm:
(-[WKWebViewConfiguration init]):
(-[WKWebViewConfiguration copyWithZone:]):
(-[WKWebViewConfiguration _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad]):
(-[WKWebViewConfiguration _setShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:]):
* UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h:

Tools:

Add an API test to verify that when the deferred async script configuration is set, async scripts will be
executed after the DOMContentLoaded event.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/RunScriptAfterDocumentLoad.mm: Added.
(TEST):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@233915 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
Source/WebCore/ChangeLog
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/Document.h
Source/WebCore/dom/ScriptRunner.cpp
Source/WebCore/dom/ScriptRunner.h
Source/WebCore/page/Settings.yaml
Source/WebKit/ChangeLog
Source/WebKit/Shared/WebPreferences.yaml
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/RunScriptAfterDocumentLoad.mm [new file with mode: 0644]

index 6493c7f..be26c8e 100644 (file)
@@ -1,3 +1,46 @@
+2018-07-18  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Add SPI to defer running async script until after document load
+        https://bugs.webkit.org/show_bug.cgi?id=187748
+        <rdar://problem/42317378>
+
+        Reviewed by Ryosuke Niwa and Tim Horton.
+
+        On watchOS, we currently observe that time-consuming async scripts can block the first paint of Reader, leaving
+        the user with a blank screen for tens of seconds. One way to mitigate this is to defer async script execution
+        until after document load (i.e. the same timing as DOMContentLoaded).
+
+        This patch introduces an SPI configuration allowing internal clients to defer execution of asynchronous script
+        until after document load; this, in combination with the parser yielding token introduced in r233891, allows
+        Safari on watchOS to avoid being blocked on slow script execution before the first paint of the Reader page on
+        most article-like pages. See below for more details.
+
+        Test: RunScriptAfterDocumentLoad.ExecutionOrderOfScriptsInDocument
+
+        * dom/Document.cpp:
+        (WebCore::Document::shouldDeferAsynchronousScriptsUntilParsingFinishes const):
+        (WebCore::Document::finishedParsing):
+
+        Notify ScriptRunner when the Document has finished parsing, and is about to fire DOMContentLoaded.
+
+        * dom/Document.h:
+        * dom/ScriptRunner.cpp:
+        (WebCore::ScriptRunner::documentFinishedParsing):
+
+        When the document is finished parsing, kick off the script execution timer if needed to run any async script
+        that has been deferred.
+
+        (WebCore::ScriptRunner::notifyFinished):
+        (WebCore::ScriptRunner::timerFired):
+
+        Instead of always taking from the list of async scripts to execute, check our document to see whether we should
+        defer this until after document load. If so, ignore `m_scriptsToExecuteSoon`.
+
+        * dom/ScriptRunner.h:
+        * page/Settings.yaml:
+
+        Add a WebCore setting for this behavior.
+
 2018-07-18  Zan Dobersek  <zdobersek@igalia.com>
 
         [Nicosia] Add debug border, repaint counter state tracking to Nicosia::CompositionLayer
index 556abca..c00ffc1 100644 (file)
@@ -5193,6 +5193,11 @@ void Document::popCurrentScript()
     m_currentScriptStack.removeLast();
 }
 
+bool Document::shouldDeferAsynchronousScriptsUntilParsingFinishes() const
+{
+    return parsing() && settings().shouldDeferAsynchronousScriptsUntilAfterDocumentLoad();
+}
+
 #if ENABLE(XSLT)
 
 void Document::scheduleToApplyXSLTransforms()
@@ -5429,6 +5434,8 @@ void Document::finishedParsing()
 
     Ref<Document> protectedThis(*this);
 
+    scriptRunner()->documentFinishedParsing();
+
     if (!m_documentTiming.domContentLoadedEventStart)
         m_documentTiming.domContentLoadedEventStart = MonotonicTime::now();
 
index b9b4fc4..0e2bb37 100644 (file)
@@ -991,6 +991,8 @@ public:
     void pushCurrentScript(HTMLScriptElement*);
     void popCurrentScript();
 
+    bool shouldDeferAsynchronousScriptsUntilParsingFinishes() const;
+
 #if ENABLE(XSLT)
     void scheduleToApplyXSLTransforms();
     void applyPendingXSLTransformsNowIfScheduled();
index 62e56f2..2f3b1a4 100644 (file)
@@ -86,6 +86,12 @@ void ScriptRunner::resume()
         m_timer.startOneShot(0_s);
 }
 
+void ScriptRunner::documentFinishedParsing()
+{
+    if (!m_scriptsToExecuteSoon.isEmpty() && !m_timer.isActive())
+        resume();
+}
+
 void ScriptRunner::notifyFinished(PendingScript& pendingScript)
 {
     if (pendingScript.element().willExecuteInOrder())
@@ -105,7 +111,9 @@ void ScriptRunner::timerFired()
     Ref<Document> protect(m_document);
 
     Vector<RefPtr<PendingScript>> scripts;
-    scripts.swap(m_scriptsToExecuteSoon);
+
+    if (!m_document.shouldDeferAsynchronousScriptsUntilParsingFinishes())
+        scripts.swap(m_scriptsToExecuteSoon);
 
     size_t numInOrderScriptsToExecute = 0;
     for (; numInOrderScriptsToExecute < m_scriptsToExecuteInOrder.size() && m_scriptsToExecuteInOrder[numInOrderScriptsToExecute]->isLoaded(); ++numInOrderScriptsToExecute)
index 67eecbe..21499aa 100644 (file)
@@ -53,6 +53,8 @@ public:
     void didBeginYieldingParser() { suspend(); }
     void didEndYieldingParser() { resume(); }
 
+    void documentFinishedParsing();
+
 private:
     void timerFired();
 
index e65a912..910b336 100644 (file)
@@ -758,3 +758,6 @@ accessibilityEventsEnabled:
 
 incompleteImageBorderEnabled:
   initial: false
+
+shouldDeferAsynchronousScriptsUntilAfterDocumentLoad:
+  initial: false
index 2578669..3fb8e6c 100644 (file)
@@ -1,3 +1,25 @@
+2018-07-18  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Add SPI to defer running async script until after document load
+        https://bugs.webkit.org/show_bug.cgi?id=187748
+        <rdar://problem/42317378>
+
+        Reviewed by Ryosuke Niwa and Tim Horton.
+
+        Add plumbing for a new ShouldDeferAsynchronousScriptsUntilAfterDocumentLoad configuration that determines
+        whether async script execution should be deferred until document load (i.e. DOMContentLoaded). This
+        configuration defaults to NO on all platforms. See WebCore ChangeLog for more detail.
+
+        * Shared/WebPreferences.yaml:
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _initializeWithConfiguration:]):
+        * UIProcess/API/Cocoa/WKWebViewConfiguration.mm:
+        (-[WKWebViewConfiguration init]):
+        (-[WKWebViewConfiguration copyWithZone:]):
+        (-[WKWebViewConfiguration _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad]):
+        (-[WKWebViewConfiguration _setShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:]):
+        * UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h:
+
 2018-07-18  Zan Dobersek  <zdobersek@igalia.com>
 
         [CoordGraphics] Start tracking Nicosia layers in CoordinatedGraphicsState
index eb0c2a3..3702660 100644 (file)
@@ -1092,6 +1092,10 @@ IncompleteImageBorderEnabled:
   type: bool
   defaultValue: false
 
+ShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:
+  type: bool
+  defaultValue: false
+
 StorageAccessAPIEnabled:
   type: bool
   defaultValue: true
index 9b4b463..9258b07 100644 (file)
@@ -547,6 +547,7 @@ static void validate(WKWebViewConfiguration *configuration)
     pageConfiguration->setDrawsBackground([_configuration _drawsBackground]);
     pageConfiguration->setControlledByAutomation([_configuration _isControlledByAutomation]);
     pageConfiguration->preferenceValues().set(WebKit::WebPreferencesKey::incompleteImageBorderEnabledKey(), WebKit::WebPreferencesStore::Value(!![_configuration _incompleteImageBorderEnabled]));
+    pageConfiguration->preferenceValues().set(WebKit::WebPreferencesKey::shouldDeferAsynchronousScriptsUntilAfterDocumentLoadKey(), WebKit::WebPreferencesStore::Value(!![_configuration _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad]));
 
 #if ENABLE(APPLICATION_MANIFEST)
     pageConfiguration->setApplicationManifest([_configuration _applicationManifest] ? [configuration _applicationManifest]->_applicationManifest.get() : nullptr);
index 8169b80..188ac16 100644 (file)
@@ -164,6 +164,7 @@ static _WKDragLiftDelay toDragLiftDelay(NSUInteger value)
     BOOL _allowMediaContentTypesRequiringHardwareSupportAsFallback;
     BOOL _colorFilterEnabled;
     BOOL _incompleteImageBorderEnabled;
+    BOOL _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad;
     BOOL _drawsBackground;
 
     RetainPtr<NSString> _overrideContentSecurityPolicy;
@@ -250,6 +251,7 @@ static _WKDragLiftDelay toDragLiftDelay(NSUInteger value)
 
     _colorFilterEnabled = NO;
     _incompleteImageBorderEnabled = NO;
+    _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad = NO;
     _drawsBackground = YES;
 
     return self;
@@ -414,6 +416,7 @@ static _WKDragLiftDelay toDragLiftDelay(NSUInteger value)
     configuration->_groupIdentifier = adoptNS([self->_groupIdentifier copyWithZone:zone]);
     configuration->_colorFilterEnabled = self->_colorFilterEnabled;
     configuration->_incompleteImageBorderEnabled = self->_incompleteImageBorderEnabled;
+    configuration->_shouldDeferAsynchronousScriptsUntilAfterDocumentLoad = self->_shouldDeferAsynchronousScriptsUntilAfterDocumentLoad;
     configuration->_drawsBackground = self->_drawsBackground;
 
     return configuration;
@@ -784,6 +787,16 @@ static NSString *defaultApplicationNameForUserAgent()
     _incompleteImageBorderEnabled = incompleteImageBorderEnabled;
 }
 
+- (BOOL)_shouldDeferAsynchronousScriptsUntilAfterDocumentLoad
+{
+    return _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad;
+}
+
+- (void)_setShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:(BOOL)shouldDeferAsynchronousScriptsUntilAfterDocumentLoad
+{
+    _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad = shouldDeferAsynchronousScriptsUntilAfterDocumentLoad;
+}
+
 - (BOOL)_drawsBackground
 {
     return _drawsBackground;
index d6955f9..ac9d5e5 100644 (file)
@@ -73,6 +73,7 @@ typedef NS_ENUM(NSUInteger, _WKDragLiftDelay) {
 @property (nonatomic, setter=_setColorFilterEnabled:) BOOL _colorFilterEnabled WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 @property (nonatomic, setter=_setIncompleteImageBorderEnabled:) BOOL _incompleteImageBorderEnabled WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 @property (nonatomic, setter=_setDrawsBackground:) BOOL _drawsBackground WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
+@property (nonatomic, setter=_setShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:) BOOL _shouldDeferAsynchronousScriptsUntilAfterDocumentLoad WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 #if TARGET_OS_IPHONE
 @property (nonatomic, setter=_setAlwaysRunsAtForegroundPriority:) BOOL _alwaysRunsAtForegroundPriority WK_API_AVAILABLE(ios(9_0));
index f0956a0..d876609 100644 (file)
@@ -1,3 +1,18 @@
+2018-07-18  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Add SPI to defer running async script until after document load
+        https://bugs.webkit.org/show_bug.cgi?id=187748
+        <rdar://problem/42317378>
+
+        Reviewed by Ryosuke Niwa and Tim Horton.
+
+        Add an API test to verify that when the deferred async script configuration is set, async scripts will be
+        executed after the DOMContentLoaded event.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/RunScriptAfterDocumentLoad.mm: Added.
+        (TEST):
+
 2018-07-18  Charlie Turner  <cturner@igalia.com>
 
         [WPE] Update WPEBackend in flatpak
index 9291e4c..3f8dc9a 100644 (file)
                F4C8797F2059D8D3009CD00B /* ScrollViewInsetTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4C8797E2059D8D3009CD00B /* ScrollViewInsetTests.mm */; };
                F4CD74C620FDACFA00DE3794 /* text-with-async-script.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4CD74C520FDACF500DE3794 /* text-with-async-script.html */; };
                F4CD74C920FDB49600DE3794 /* TestURLSchemeHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4CD74C820FDB49600DE3794 /* TestURLSchemeHandler.mm */; };
+               F4D2986E20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D2986D20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm */; };
                F4D4F3B61E4E2BCB00BB2767 /* DataInteractionSimulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D4F3B41E4E2BCB00BB2767 /* DataInteractionSimulator.mm */; };
                F4D4F3B91E4E36E400BB2767 /* DataInteractionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D4F3B71E4E36E400BB2767 /* DataInteractionTests.mm */; };
                F4D5E4E81F0C5D38008C1A49 /* dragstart-clear-selection.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4D5E4E71F0C5D27008C1A49 /* dragstart-clear-selection.html */; };
                F4CD74C520FDACF500DE3794 /* text-with-async-script.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "text-with-async-script.html"; sourceTree = "<group>"; };
                F4CD74C720FDB49600DE3794 /* TestURLSchemeHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestURLSchemeHandler.h; sourceTree = "<group>"; };
                F4CD74C820FDB49600DE3794 /* TestURLSchemeHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TestURLSchemeHandler.mm; sourceTree = "<group>"; };
+               F4D2986D20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RunScriptAfterDocumentLoad.mm; sourceTree = "<group>"; };
                F4D4F3B41E4E2BCB00BB2767 /* DataInteractionSimulator.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DataInteractionSimulator.mm; sourceTree = "<group>"; };
                F4D4F3B51E4E2BCB00BB2767 /* DataInteractionSimulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataInteractionSimulator.h; sourceTree = "<group>"; };
                F4D4F3B71E4E36E400BB2767 /* DataInteractionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DataInteractionTests.mm; sourceTree = "<group>"; };
                                CD9E292B1C90A71F000BB800 /* RequiresUserActionForPlayback.mm */,
                                51C8E1A41F26AC5400BF731B /* ResourceLoadStatistics.mm */,
                                A180C0F91EE67DF000468F47 /* RunOpenPanel.mm */,
+                               F4D2986D20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm */,
                                CE0947362063223B003C9BA0 /* SchemeRegistry.mm */,
                                51EB12931FDF050500A5A1BD /* ServiceWorkerBasic.mm */,
                                37BCA61B1B596BA9002012CA /* ShouldOpenExternalURLsInNewWindowActions.mm */,
                                46E816F81E79E29C00375ADC /* RestoreStateAfterTermination.mm in Sources */,
                                F418BE151F71B7DC001970E6 /* RoundedRectTests.cpp in Sources */,
                                A180C0FA1EE67DF000468F47 /* RunOpenPanel.mm in Sources */,
+                               F4D2986E20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm in Sources */,
                                CDCFA7AA1E45183200C2433D /* SampleMap.cpp in Sources */,
                                CE0947372063223B003C9BA0 /* SchemeRegistry.mm in Sources */,
                                7CCE7F121A411AE600447C4C /* ScrollPinningBehaviors.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/RunScriptAfterDocumentLoad.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/RunScriptAfterDocumentLoad.mm
new file mode 100644 (file)
index 0000000..51c6849
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#import "config.h"
+
+#if WK_API_ENABLED
+
+#import "PlatformUtilities.h"
+#import "TestNavigationDelegate.h"
+#import "TestURLSchemeHandler.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKWebViewConfigurationPrivate.h>
+#import <wtf/RetainPtr.h>
+#import <wtf/Seconds.h>
+
+static const char* markup =
+    "<head>"
+    "<script>function scriptLoaded(element) { webkit.messageHandlers.testHandler.postMessage(`${element.getAttribute('src')} loaded`) }</script>"
+    "<script onload='scriptLoaded(this)' src='async.js' async></script>"
+    "</head>"
+    "<body>"
+    "<script>document.addEventListener('DOMContentLoaded', () => webkit.messageHandlers.testHandler.postMessage('DOMContentLoaded'))</script>"
+    "<script onload='scriptLoaded(this)' src='defer.js' defer></script>"
+    "<script onload='scriptLoaded(this)' src='sync.js'></script>"
+    "</body>";
+
+TEST(RunScriptAfterDocumentLoad, ExecutionOrderOfScriptsInDocument)
+{
+    auto schemeHandler = adoptNS([[TestURLSchemeHandler alloc] init]);
+    auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+    [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"custom"];
+    [configuration _setShouldDeferAsynchronousScriptsUntilAfterDocumentLoad:YES];
+    [schemeHandler setStartURLSchemeTaskHandler:^(WKWebView *, id<WKURLSchemeTask> task) {
+        auto responseBlock = [task = retainPtr(task)] {
+            NSURL *requestURL = [task request].URL;
+            NSString *script = [NSString stringWithFormat:@"webkit.messageHandlers.testHandler.postMessage('Running %@')", requestURL.absoluteString.lastPathComponent];
+            auto response = adoptNS([[NSURLResponse alloc] initWithURL:requestURL MIMEType:@"text/javascript" expectedContentLength:[script length] textEncodingName:nil]);
+            [task didReceiveResponse:response.get()];
+            [task didReceiveData:[script dataUsingEncoding:NSUTF8StringEncoding]];
+            [task didFinish];
+        };
+
+        if ([[task request].URL.absoluteString containsString:@"async.js"]) {
+            responseBlock();
+            return;
+        }
+
+        // Delay request responses for the other two scripts for a short duration to ensure that in the absence
+        // of the deferred asynchronous scripts configuration flag, we will normally execute the asynchronous script
+        // before the other scripts.
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.25_s).nanoseconds()), dispatch_get_main_queue(), responseBlock);
+    }];
+
+    auto messages = adoptNS([NSMutableArray new]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 200, 200) configuration:configuration.get()]);
+    for (NSString *message in @[ @"Running async.js", @"Running defer.js", @"Running sync.js", @"DOMContentLoaded", @"async.js loaded", @"defer.js loaded", @"sync.js loaded" ]) {
+        [webView performAfterReceivingMessage:message action:[message = adoptNS(message.copy), messages] {
+            [messages addObject:message.get()];
+        }];
+    }
+
+    [webView loadHTMLString:@(markup) baseURL:[NSURL URLWithString:@"custom://"]];
+    [webView _test_waitForDidFinishNavigation];
+
+    // Verify that the asynchronous script is executed after document load, the synchronous script is executed before the
+    // deferred script, and the deferred script is executed prior to document load.
+    EXPECT_EQ(7U, [messages count]);
+    EXPECT_WK_STREQ("Running sync.js", [messages objectAtIndex:0]);
+    EXPECT_WK_STREQ("sync.js loaded", [messages objectAtIndex:1]);
+    EXPECT_WK_STREQ("Running defer.js", [messages objectAtIndex:2]);
+    EXPECT_WK_STREQ("defer.js loaded", [messages objectAtIndex:3]);
+    EXPECT_WK_STREQ("DOMContentLoaded", [messages objectAtIndex:4]);
+    EXPECT_WK_STREQ("Running async.js", [messages objectAtIndex:5]);
+    EXPECT_WK_STREQ("async.js loaded", [messages objectAtIndex:6]);
+}
+
+#endif // WK_API_ENABLED