Bug 2062317, be more synchronous with DOMContentLoaded, r=farre

Differential Revision: https://phabricator.services.mozilla.com/D321732
This commit is contained in:
Olli Pettay
2026-09-09 14:18:48 +00:00
committed by opettay@mozilla.com
parent c70773c334
commit 91972506c1
24 changed files with 281 additions and 56 deletions
+1 -1
View File
@@ -10342,7 +10342,7 @@ nsresult nsDocShell::CompleteInitialAboutBlankLoad(
// Mechanisms in Document will force a load from EndLoad()
// even if there are still blockers.
doc->EndLoad();
doc->EndLoad(/* aFireDOMContentLoadedSync = */ true);
// Can't assert any postcondition, because the load event
// handler may have started loading something new in this
// docshell.
+39 -14
View File
@@ -8770,7 +8770,7 @@ void Document::MozSetImageElement(const nsAString& aImageElementId,
}
}
void Document::DispatchContentLoadedEvents() {
void Document::DispatchContentLoadedEvents(bool aFinishSync) {
// If you add early returns from this method, make sure you're
// calling UnblockOnload properly.
@@ -8882,6 +8882,19 @@ void Document::DispatchContentLoadedEvents() {
}
}
if (aFinishSync) {
FinishDOMContentLoaded();
return;
}
// Keep the load event on a task, so that its timing does not change.
nsCOMPtr<nsIRunnable> ev =
NewRunnableMethod("Document::FinishDOMContentLoaded", this,
&Document::FinishDOMContentLoaded);
Dispatch(ev.forget());
}
void Document::FinishDOMContentLoaded() {
if (mSetCompleteAfterDOMContentLoaded) {
SetReadyStateInternal(ReadyState::READYSTATE_COMPLETE);
mSetCompleteAfterDOMContentLoaded = false;
@@ -8890,7 +8903,7 @@ void Document::DispatchContentLoadedEvents() {
UnblockOnload(true);
}
void Document::EndLoad() {
void Document::EndLoad(bool aFireDOMContentLoadedSync) {
bool turnOnEditing =
mParser && (IsInDesignMode() || mContentEditableCount > 0);
@@ -8938,7 +8951,7 @@ void Document::EndLoad() {
}
mDidCallBeginLoad = false;
UnblockDOMContentLoaded();
UnblockDOMContentLoaded(aFireDOMContentLoadedSync);
if (turnOnEditing) {
EditingStateChanged();
@@ -8963,7 +8976,7 @@ void Document::EndLoad() {
}
}
void Document::UnblockDOMContentLoaded() {
void Document::UnblockDOMContentLoaded(bool aFireSync) {
MOZ_ASSERT(mBlockDOMContentLoaded);
if (--mBlockDOMContentLoaded != 0 || mDidFireDOMContentLoaded) {
return;
@@ -8974,17 +8987,28 @@ void Document::UnblockDOMContentLoaded() {
mDidFireDOMContentLoaded = true;
MOZ_RELEASE_ASSERT(NS_IsMainThread());
MOZ_ASSERT(IsInitialDocument() || mReadyState == READYSTATE_INTERACTIVE);
if (!mSynchronousDOMContentLoaded) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!IsInitialDocument());
nsCOMPtr<nsIRunnable> ev =
NewRunnableMethod("Document::DispatchContentLoadedEvents", this,
&Document::DispatchContentLoadedEvents);
Dispatch(ev.forget());
} else {
DispatchContentLoadedEvents();
// These documents need the load event unblocked before we return.
if (mSynchronousDOMContentLoaded) {
MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
DispatchContentLoadedEvents(/* aFinishSync = */ true);
return;
}
if (aFireSync &&
StaticPrefs::dom_document_domcontentloaded_synchronous_enabled()) {
nsContentUtils::AddScriptRunner(
NewRunnableMethod<bool>("Document::DispatchContentLoadedEvents", this,
&Document::DispatchContentLoadedEvents, false));
return;
}
MOZ_ASSERT(!IsInitialDocument());
Dispatch(NewRunnableMethod<bool>("Document::DispatchContentLoadedEvents",
this, &Document::DispatchContentLoadedEvents,
true));
}
void Document::ElementStateChanged(Element* aElement, ElementState aStateMask) {
@@ -15326,7 +15350,8 @@ class UnblockParsingPromiseHandler final : public PromiseNativeHandler {
// parser state for this document. Maybe someone caused it to stop being
// parsed, so CreatorParserOrNull() is returning null, but we still want
// to unblock these.
mDocument->UnblockDOMContentLoaded();
// Async, because this also runs from our destructor.
mDocument->UnblockDOMContentLoaded(/* aFireSync = */ false);
mDocument->UnblockOnload(false);
}
mParser = nullptr;
+9 -3
View File
@@ -1670,7 +1670,9 @@ class Document : public nsINode,
NotNull<const Encoding*>& aEncoding,
nsHtml5TreeOpExecutor* aExecutor);
MOZ_CAN_RUN_SCRIPT void DispatchContentLoadedEvents();
MOZ_CAN_RUN_SCRIPT void DispatchContentLoadedEvents(bool aFinishSync);
// Unblocks the load event. An aborted load also gets readyState complete.
MOZ_CAN_RUN_SCRIPT void FinishDOMContentLoaded();
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
MOZ_CAN_RUN_SCRIPT_BOUNDARY void DispatchPageTransition(
@@ -2240,7 +2242,10 @@ class Document : public nsINode,
uint32_t UpdateNestingLevel() { return mUpdateNestLevel; }
void BeginLoad();
virtual void EndLoad();
// aFireDOMContentLoadedSync must be false for a terminated parse.
// See bug 344305.
MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void EndLoad(
bool aFireDOMContentLoadedSync);
enum ReadyState {
READYSTATE_UNINITIALIZED = 0,
@@ -2643,7 +2648,8 @@ class Document : public nsINode,
void BlockDOMContentLoaded() { ++mBlockDOMContentLoaded; }
MOZ_CAN_RUN_SCRIPT_BOUNDARY void UnblockDOMContentLoaded();
// aFireSync false fires DOMContentLoaded from a task. See bug 344305.
MOZ_CAN_RUN_SCRIPT_BOUNDARY void UnblockDOMContentLoaded(bool aFireSync);
/**
* Notification that the page has been shown, for documents which are loaded
@@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script>
document.addEventListener("DOMContentLoaded", () => {
// document.open() clears the content while DOMContentLoaded is being fired.
document.open();
document.write(
"<!DOCTYPE html><html><body><script>" +
"parent.postMessage('reopened', '*');" +
"<\/script></body></html>");
document.close();
});
</script>
</head>
<body></body>
</html>
@@ -0,0 +1,13 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script>
document.addEventListener("DOMContentLoaded",
() => parent.postMessage("DOMContentLoaded", "*"));
// Terminate the parse, see bug 344305.
window.stop();
</script>
</head>
<body></body>
</html>
+6
View File
@@ -12,6 +12,8 @@ support-files = [
"iframe_main_bug1022229.html",
"iframe_sandbox_bug1022229.html",
"file_empty.html",
"file_domcontentloaded_document_open.html",
"file_domcontentloaded_terminated_parse.html",
"iframe_postMessage_solidus.html",
"file_setname.html",
"345339_iframe.html",
@@ -352,6 +354,10 @@ skip-if = [
"http3",
]
["test_domcontentloaded_document_open.html"]
["test_domcontentloaded_terminated_parse.html"]
["test_domparser_null_char.html"]
["test_domparsing.html"]
@@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test document.open() from a DOMContentLoaded listener</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<iframe id="f"></iframe>
<script>
SimpleTest.waitForExplicitFinish();
window.addEventListener("message", event => {
is(event.data, "reopened",
"the content written from a DOMContentLoaded listener was parsed");
SimpleTest.finish();
});
document.getElementById("f").src = "file_domcontentloaded_document_open.html";
</script>
</body>
</html>
@@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test DOMContentLoaded after a terminated parse</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<iframe id="f"></iframe>
<script>
SimpleTest.waitForExplicitFinish();
window.addEventListener("message", event => {
is(event.data, "DOMContentLoaded",
"DOMContentLoaded fires after the parse was terminated");
SimpleTest.finish();
});
document.getElementById("f").src = "file_domcontentloaded_terminated_parse.html";
</script>
</body>
</html>
+60
View File
@@ -24,6 +24,7 @@ const server = XPCShellContentUtils.createHttpServer({
// XML document with only a <script> tag as the document element.
const PAGE_URL = "http://example.com/";
const DCL_PAGE_URL = "http://example.com/blockdcl";
server.registerPathHandler("/", (request, response) => {
response.setHeader("Content-Type", "application/xhtml+xml");
response.write(String.raw`<!DOCTYPE html>
@@ -31,6 +32,11 @@ server.registerPathHandler("/", (request, response) => {
`);
});
server.registerPathHandler("/blockdcl", (request, response) => {
response.setHeader("Content-Type", "text/html");
response.write("<!DOCTYPE html><html><body>blockdcl</body></html>");
});
let resolveResumeScriptPromise;
let resumeScriptPromise = new Promise(resolve => {
resolveResumeScriptPromise = resolve;
@@ -119,3 +125,57 @@ add_task(async function test_nested_blockParser() {
let page = await pagePromise;
await page.close();
});
// Tests that blockParsing() also blocks DOMContentLoaded, and that
// DOMContentLoaded is fired once the blocker promise settles.
add_task(async function test_blockParsing_blocksDOMContentLoaded() {
let resolveBlockerPromise;
let dclFired = false;
let dclPromise;
let docElementPromise = TestUtils.topicObserved(
"document-element-inserted",
doc => {
if (doc.location.href !== DCL_PAGE_URL) {
return false;
}
let blockerPromise = new Promise(resolve => {
resolveBlockerPromise = resolve;
});
doc.blockParsing(blockerPromise);
dclPromise = new Promise(resolve => {
doc.addEventListener(
"DOMContentLoaded",
() => {
dclFired = true;
resolve();
},
{ once: true }
);
});
return true;
}
);
let pagePromise = XPCShellContentUtils.loadContentPage(DCL_PAGE_URL, {
remote: false,
});
await docElementPromise;
// Make some trips through the event loop to be safe.
await delay();
await delay();
Assert.ok(!dclFired, "DOMContentLoaded is blocked while parsing is blocked");
resolveBlockerPromise();
await dclPromise;
Assert.ok(dclFired, "DOMContentLoaded is fired once the blocker settles");
let page = await pagePromise;
await page.close();
});
@@ -694,7 +694,7 @@ nsresult PrototypeDocumentContentSink::DoneWalking() {
doc->SetScrollToRef(mDocument->GetDocumentURI());
doc->EndLoad();
doc->EndLoad(/* aFireDOMContentLoadedSync = */ true);
return NS_OK;
}
+6 -4
View File
@@ -4638,9 +4638,11 @@ void ScriptLoader::ProcessPendingRequests(bool aAllowBypassingParserBlocking) {
if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest &&
mNonAsyncExternalScriptInsertedRequests.isEmpty() &&
mXSLTRequests.isEmpty() && mDeferRequests.isEmpty() &&
MaybeRemovedDeferRequests()) {
return ProcessPendingRequests();
mXSLTRequests.isEmpty() && mDeferRequests.isEmpty()) {
const RefPtr<ScriptLoader> self = this;
if (self->MaybeRemovedDeferRequests()) {
return self->ProcessPendingRequests();
}
}
if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest &&
@@ -5669,7 +5671,7 @@ void ScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) {
bool ScriptLoader::MaybeRemovedDeferRequests() {
if (mDeferRequests.isEmpty() && mDocument && mBlockingDOMContentLoaded) {
mBlockingDOMContentLoaded = false;
mDocument->UnblockDOMContentLoaded();
mDocument->UnblockDOMContentLoaded(/* aFireSync = */ true);
return true;
}
return false;
+1 -1
View File
@@ -862,7 +862,7 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface {
void AddDeferRequest(ScriptLoadRequest* aRequest);
void AddAsyncRequest(ScriptLoadRequest* aRequest);
bool MaybeRemovedDeferRequests();
MOZ_CAN_RUN_SCRIPT bool MaybeRemovedDeferRequests();
bool ShouldApplyDelazifyStrategy(ScriptLoadRequest* aRequest);
void ApplyDelazifyStrategy(JS::CompileOptions* aOptions);
+2 -2
View File
@@ -298,11 +298,11 @@ nsresult XMLDocument::StartDocumentLoad(
return NS_OK;
}
void XMLDocument::EndLoad() {
void XMLDocument::EndLoad(bool aFireDOMContentLoadedSync) {
mChannelIsPending = false;
mSynchronousDOMContentLoaded = mLoadedAsData;
Document::EndLoad();
Document::EndLoad(aFireDOMContentLoadedSync);
if (mSynchronousDOMContentLoaded) {
mSynchronousDOMContentLoaded = false;
Document::SetReadyStateInternal(Document::READYSTATE_COMPLETE);
+2 -1
View File
@@ -40,7 +40,8 @@ class XMLDocument : public Document {
bool aReset = true) override;
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void EndLoad() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void EndLoad(
bool aFireDOMContentLoadedSync) override;
virtual nsresult Init(nsIPrincipal* aPrincipal,
nsIPrincipal* aPartitionedPrincipal) override;
+3 -2
View File
@@ -305,6 +305,7 @@ nsXMLContentSink::DidBuildModel(bool aTerminated) {
mDocument->RemoveObserver(this);
mIsDocumentObserver = false;
const RefPtr<nsXMLContentSink> kungFuDeathGrip(this);
RefPtr<Document> doc = mDocument;
if (!mDeferredLayoutStart && doc->IsBeingUsedAsImage()) {
// Eagerly layout image documents, so that layout-triggered loads have a
@@ -312,7 +313,7 @@ nsXMLContentSink::DidBuildModel(bool aTerminated) {
doc->FlushPendingNotifications(FlushType::Layout);
}
doc->EndLoad();
doc->EndLoad(/* aFireDOMContentLoadedSync = */ !aTerminated);
DropParserAndPerfHint();
}
@@ -402,7 +403,7 @@ nsresult nsXMLContentSink::OnTransformDone(Document* aSourceDocument,
ScrollToRef();
}
originalDocument->EndLoad();
originalDocument->EndLoad(/* aFireDOMContentLoadedSync = */ true);
if (blockingOnload) {
// This UnblockOnload call corresponds to the BlockOnload call in
// nsContentSink::WillBuildModelImpl.
+6
View File
@@ -2817,6 +2817,12 @@
value: true
mirror: always
# Fire DOMContentLoaded at the end of parsing, instead of from a task.
- name: dom.document.domcontentloaded.synchronous.enabled
type: bool
value: true
mirror: always
# Enable/disable Gecko specific edit commands
- name: dom.document.edit_command.contentReadOnly.enabled
type: bool
+8 -4
View File
@@ -258,7 +258,8 @@ nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) {
// This comes from nsXMLContentSink and the old (now removed)
// nsHTMLContentSink. If this parser has been marked as broken, treat the end
// of parse as forced termination.
DidBuildModelImpl(aTerminated || NS_FAILED(IsBroken()));
const bool terminated = aTerminated || NS_FAILED(IsBroken());
DidBuildModelImpl(terminated);
bool destroying = true;
if (mDocShell) {
@@ -290,8 +291,11 @@ nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) {
// We may not have called BeginLoad() if loading is terminated before
// OnStartRequest call.
// EndLoad below can run script which drops the parser.
const RefPtr<nsHtml5Parser> parser = GetParser();
if (mStarted) {
mDocument->EndLoad();
mDocument->EndLoad(/* aFireDOMContentLoadedSync = */ !terminated);
// Log outcome only for top-level content navigations in order to
// avoid noise from ad iframes.
@@ -311,7 +315,7 @@ nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) {
// error pages.
bool httpOk = false;
nsCOMPtr<nsIChannel> channel;
nsresult rv = GetParser()->GetChannel(getter_AddRefs(channel));
nsresult rv = parser->GetChannel(getter_AddRefs(channel));
if (NS_SUCCEEDED(rv) && channel) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
if (httpChannel) {
@@ -422,7 +426,7 @@ nsHtml5TreeOpExecutor::DidBuildModel(bool aTerminated) {
// before this executor's nsHtml5Parser has been made unreachable from its
// nsHTMLDocument. (mDocument->EndLoad() above drops the parser from the
// document.)
GetParser()->DropStreamParser();
parser->DropStreamParser();
DropParserAndPerfHint();
#ifdef GATHER_DOCWRITE_STATISTICS
printf("UNSAFE SCRIPTS: %d\n", sUnsafeDocWrites);
@@ -0,0 +1,2 @@
[DOMContentLoaded-defer-task-order.tentative.html]
prefs: [dom.document.domcontentloaded.synchronous.enabled:true]
@@ -1,3 +1,6 @@
[DOMContentLoaded-defer.html]
disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1242128
prefs: [dom.document.domcontentloaded.synchronous.enabled:true]
# This test asserts an order which is not specified, see
# https://github.com/web-platform-tests/wpt/issues/4267
[The end: DOMContentLoaded and defer scripts]
expected: FAIL
@@ -0,0 +1,28 @@
<!doctype html>
<meta charset=utf-8>
<title>The end: DOMContentLoaded and a task queued from a defer script</title>
<link rel=help href="https://html.spec.whatwg.org/multipage/#the-end">
<link rel=help href="https://github.com/web-platform-tests/wpt/issues/4267">
<!--
Tentative: the order between DOMContentLoaded, which the specification queues
on the DOM manipulation task source, and a timer task is not specified.
DOMContentLoaded-defer.html asserts the opposite order.
-->
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
var order = [];
document.addEventListener("DOMContentLoaded", () => order.push("DOMContentLoaded"));
var t = async_test(
"DOMContentLoaded fires before a task queued from a defer script");
function check() {
t.step(() => {
assert_array_equals(order, ["defer", "DOMContentLoaded", "timeout"]);
});
t.done();
}
</script>
<script defer src="data:text/javascript,order.push('defer');t.step_timeout(()=>{order.push('timeout');check()},0)"></script>
@@ -4,19 +4,17 @@
<link rel="author" title="Divyansh Mangal" href="mailto:dmangal@microsoft.com">
<link rel="help" href="https://w3c.github.io/svgwg/svg2-draft/linking.html#InterfaceSVGAElement">
<title> Rel attribute with noreferrer value </title>
<svg>
<a id="test" href="resources/a.rel-noreferrer-policy-target.html" rel="noreferrer"></a>
<script>
var anchorElement = document.getElementById('test');
<body>
<script>
// The link is in an iframe, because a test must not navigate its own top level
// browsing context.
async_test(t => {
window.addEventListener("message", t.step_func_done(event => {
assert_equals(event.data, "");
}));
// Simulate a click event
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
// Dispatch the event to the anchor element
anchorElement.dispatchEvent(event);
</script>
</svg>
const iframe = document.createElement("iframe");
iframe.src = "resources/a.rel-noreferrer-policy-frame.html";
document.body.appendChild(iframe);
}, "No Referrer policy attribute on svg anchor element is applied");
</script>
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<svg>
<a id="test" href="a.rel-noreferrer-policy-target.html" rel="noreferrer"></a>
<script>
document.getElementById("test").dispatchEvent(new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true
}));
</script>
</svg>
@@ -1,10 +1,5 @@
<!DOCTYPE html>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<link rel="author" title="Divyansh Mangal" href="mailto:dmangal@microsoft.com">
<link rel="help" href="https://w3c.github.io/svgwg/svg2-draft/linking.html#InterfaceSVGAElement">
<script>
test(function () {
assert_equals("", document.referrer);
}, "No Referrer policy attribute on svg anchor element is applied");
parent.postMessage(document.referrer, "*");
</script>
+2
View File
@@ -64,6 +64,8 @@
}
async function runTests() {
// The drag needs a focused window.
await SimpleTest.promiseFocus();
await SpecialPowers.contentTransformsReceived(window);
for (let currentTest of tests) {
await test(currentTest.actual, currentTest.expected);