Merge pull request #2843 from navidemad/fix-a10-stylesheet-deferred-flush

page: flush deferred transfers after a synchronous stylesheet fetch
This commit is contained in:
Karl Seguin
2026-07-01 12:28:10 +08:00
committed by GitHub
4 changed files with 74 additions and 0 deletions

View File

@@ -2048,6 +2048,20 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
};
const http_client = &session.browser.http_client;
// `syncRequest` below registers a blocking request for this frame, which
// makes the DeferringLayer hold back the completion callbacks of every
// OTHER in-flight transfer for the frame (e.g. a `<script defer>` still
// loading) so they don't run JS while we're on the parser stack. Those
// deferred completions must be flushed once the sync fetch returns —
// otherwise a `<script defer>` whose fetch finishes during this window is
// left at `complete == false` forever, the deferred-script queue never
// drains, and `documentIsLoaded` (readyState -> "interactive",
// DOMContentLoaded, the load event) never fires. The blocking-`<script>`
// path (ScriptManager.addFromElement) and the worker path already flush;
// the external-stylesheet path must too.
defer http_client.deferring_layer.flushFrame(self._frame_id);
var headers = try http_client.newHeaders();
try headers.add("Accept: text/css,*/*;q=0.1");
try self.headersForRequest(&headers);

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<head>
<script src="../testing.js"></script>
<script id=deferred_script_drains_after_sync_stylesheet>
// Runs during parsing, before the body's deferred script + stylesheet.
// Crucially this is the LAST blocking <script> in the document: nothing
// blocking follows the <link rel=stylesheet> below, so the only thing
// that can flush the deferred script's completion is the stylesheet
// fetch itself (the regression). A blocking <script> after the <link>
// would call flushFrame on its own and hide the bug.
testing.expectEqual('loading', document.readyState);
testing.expectEqual(undefined, window.__deferRan);
testing.onload(() => {
// Reaching load means the deferred-script queue drained: the deferred
// script ran and the document advanced out of "loading".
testing.expectEqual(true, window.__deferRan);
testing.expectEqual('complete', document.readyState);
});
</script>
</head>
<body>
<!--
Regression: a `<script defer>` followed by an external
`<link rel=stylesheet>` (external-stylesheet loading enabled).
Frame.loadExternalStylesheet fetches the sheet SYNCHRONOUSLY, registering
a blocking request for the frame. While that blocking request is active,
the DeferringLayer holds back the completion callbacks of every OTHER
in-flight transfer for the frame — including the deferred script that
started loading just above. Those deferred completions must be flushed
once the sync fetch returns; otherwise the script stays at
`complete == false`, the deferred-script queue never drains, and
DOMContentLoaded / the load event / readyState -> "complete" never fire.
-->
<script defer src="/defer-marker.js"></script>
<link rel="stylesheet" href="/styles/visibility.css">
</body>

View File

@@ -288,3 +288,14 @@ test "WebApi: HTML.Link external stylesheet" {
defer filter.deinit();
try testing.htmlRunner("css/external_stylesheet.html", .{ .load_external_stylesheets = true });
}
// Regression: a synchronous external-stylesheet fetch must not strand the
// completion of an in-flight <script defer> (deferred by the blocking-request
// window). Otherwise the deferred-script queue never drains and the document
// is stuck at readyState "loading". See Frame.loadExternalStylesheet's
// flushFrame call.
test "WebApi: HTML.Link deferred script then external stylesheet" {
const filter: testing.LogFilter = .init(&.{.http});
defer filter.deinit();
try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_external_stylesheets = true });
}

View File

@@ -715,6 +715,17 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
if (std.mem.eql(u8, path, "/defer-marker.js")) {
// A deferred script body. Used by the regression test for a
// <script defer> whose completion is deferred by a later
// <link rel=stylesheet>'s synchronous fetch — it must still execute.
return req.respond("window.__deferRan = true;", .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "application/javascript" },
},
});
}
if (std.mem.eql(u8, path, "/xhr/500")) {
return req.respond("Internal Server Error", .{
.status = .internal_server_error,