mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-01 02:06:17 -04:00
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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
4
src/browser/tests/runner/iframe_lazy.html
Normal file
4
src/browser/tests/runner/iframe_lazy.html
Normal 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>
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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..]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user