perf: Iframes with loading=lazy don't delay parent's "load" event

When an iframe has "loading=lazy" it no longer delays the parent's "load" event.
It's still loaded (subject to `--disable-subframes` parameter).

Many sites use loading=lazy for non-critical content (e.g. ads). On Chrome/
Firefox, these are only loaded when they're in the viewport, so sites usually
can't depend/rely on them being loaded.
This commit is contained in:
Karl Seguin
2026-07-07 11:17:36 +08:00
parent c1dfb09f04
commit 032cbc3846
6 changed files with 74 additions and 10 deletions

View File

@@ -283,6 +283,11 @@ _pending_loads: u32,
_parent_notified: bool = false,
// whether our parent's load event is waiting for us to load. For a root page,
// this is meaningless. For an iframe, it's false when loading=lazy. We'll still
// load the iframe, but we won't delay the parent
_delays_parent_load: bool = true,
_type: enum { root, frame }, // only used for logs right now
_req_id: u32 = 0,
_navigated_options: ?NavigatedOpts = null,
@@ -1012,7 +1017,7 @@ pub fn scriptsCompletedLoading(self: *Frame) void {
self.pendingLoadCompleted();
}
pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void {
pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame, delays_load: bool) void {
// When parsing HTML, fire any load event for an iframe on the next tick.
const parsing_html = switch (self._parse_state) {
.html => true,
@@ -1022,7 +1027,9 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void {
self.queueElementEvent(iframe._proto, .load) catch |err| {
log.err(.frame, "iframe queue load", .{ .err = err, .url = iframe._src });
};
self.pendingLoadCompleted();
if (delays_load) {
self.pendingLoadCompleted();
}
return;
}
@@ -1040,7 +1047,9 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void {
};
}
self.pendingLoadCompleted();
if (delays_load) {
self.pendingLoadCompleted();
}
}
fn pendingLoadCompleted(self: *Frame) void {
@@ -1125,7 +1134,7 @@ fn notifyParentLoadComplete(self: *Frame) void {
}
self._parent_notified = true;
parent.iframeCompletedLoading(self.iframe.?);
parent.iframeCompletedLoading(self.iframe.?, self._delays_parent_load);
}
fn frameHeaderDoneCallback(response: HttpClient.Response) !HttpClient.HeaderResult {
@@ -1677,7 +1686,11 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
try Frame.init(new_frame, frame_id, self._page, .{ .parent = self });
errdefer new_frame.deinit();
self._pending_loads += 1;
const delays_load = iframe.isLazyLoading() == false;
new_frame._delays_parent_load = delays_load;
if (delays_load) {
self._pending_loads += 1;
}
new_frame.iframe = iframe;
iframe._window = new_frame.window;
errdefer iframe._window = null;
@@ -1728,7 +1741,9 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
_ = self.child_frames.swapRemove(idx);
}
log.warn(.frame, "iframe navigate failure", .{ .url = url, .err = err });
self._pending_loads -= 1;
if (delays_load) {
self._pending_loads -= 1;
}
iframe._window = null;
return error.IFrameLoadError;
};

View File

@@ -492,3 +492,27 @@ test "Runner: networkidle notifies child frames" {
try testing.expectEqual(true, child._notified_network_idle == .done);
}
}
test "Runner: lazy iframe does not delay the load event" {
const page = try testing.pageTest("runner/iframe_lazy.html", .{ .wait_until_done = false });
defer page.close();
var runner = page.session.runner(.{});
try runner.waitForFrame(page.frame_id, 2000, .{ .until = .load });
const frame = page.frame().?;
try testing.expectEqual(true, frame._load_state == .complete);
try testing.expectEqual(2, frame.child_frames.items.len);
// The lazy child's delayed response is still in flight: the parent's
// load event fired without waiting for it.
const lazy_child = frame.child_frames.items[0];
try testing.expectEqual(false, lazy_child._delays_parent_load);
try testing.expectEqual(false, lazy_child._load_state == .complete);
// It still loads to completion and notifies the parent (which fires the
// iframe element's load event, but doesn't touch _pending_loads).
try runner.waitForFrame(page.frame_id, 3000, .{ .until = .done });
try testing.expectEqual(true, lazy_child._load_state == .complete);
try testing.expectEqual(true, lazy_child._parent_notified);
}

View File

@@ -655,9 +655,14 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
errdefer iframe._window = null;
const parent_notified = frame._parent_notified;
if (parent_notified) {
// we already notified the parent that we had loaded
// Should this frame's navigation delay the parent's load event? If it was
// already holding it, then that just carries over. If it wasn't delaying
// it (maybe because it completed already), then it should start, UNLESS
// the iframe is lazy loaded.
const had_hold = !frame._parent_notified and frame._delays_parent_load;
const delays_load = had_hold or !iframe.isLazyLoading();
const new_hold = delays_load and !had_hold;
if (new_hold) {
parent._pending_loads += 1;
}
@@ -682,13 +687,14 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation)
try Frame.init(frame, frame_id, page, .{ .parent = parent, .reuse_window = reuse_window });
errdefer {
if (parent_notified) {
if (new_hold) {
parent._pending_loads -= 1;
}
frame.deinit();
}
frame.iframe = iframe;
frame._delays_parent_load = delays_load;
iframe._window = frame.window;
frame.navigate(qn.url, qn.opts) catch |err| {

View File

@@ -0,0 +1,4 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<iframe loading="lazy" src="runner1.html?delay_ms=500"></iframe>
<iframe src="about:blank"></iframe>

View File

@@ -16,6 +16,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../../js/js.zig");
const Frame = @import("../../../Frame.zig");
const Window = @import("../../Window.zig");
@@ -47,6 +49,12 @@ pub fn getContentDocument(self: *const IFrame) ?*Document {
return window._document;
}
// loading=lazy iframes are still but don't delay the page's "load" event
pub fn isLazyLoading(self: *IFrame) bool {
const loading = self.asElement().getAttributeSafe(comptime .wrap("loading")) orelse return false;
return std.ascii.eqlIgnoreCase(loading, "lazy");
}
pub fn getSrc(self: *IFrame, frame: *Frame) ![]const u8 {
if (self._src.len == 0) return "";
return self.asNode().resolveURLReflect(self._src, frame, .{});

View File

@@ -859,6 +859,13 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
}
if (std.mem.startsWith(u8, path, "/src/browser/tests/")) {
if (std.mem.indexOf(u8, path, "delay_ms=")) |pos| {
const digits_start = pos + "delay_ms=".len;
var end = digits_start;
while (end < path.len and std.ascii.isDigit(path[end])) : (end += 1) {}
const delay_ms = std.fmt.parseInt(u64, path[digits_start..end], 10) catch 0;
std.Thread.sleep(delay_ms * std.time.ns_per_ms);
}
// strip off leading / so that it's relative to CWD
return TestHTTPServer.sendFile(req, path[1..]);
}