From c4e85c327747500e383658280a0769a8ad15e890 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 10 Mar 2026 14:57:40 +0800 Subject: [PATCH] Add a hasDirectListeners to EventManager Allows checking if a direct listener exists, if it doesn't, event creation can be skipped. I looked at a couple sites, the benefits of this is small. Most sites don't seem to trigger that many direct dispatches and when they do, they seem to have a listener 50-75% of the time. --- src/browser/EventManager.zig | 23 +++++++++++++++++ src/browser/Page.zig | 29 +++++++++------------- src/browser/webapi/AbortSignal.zig | 12 ++++----- src/browser/webapi/EventTarget.zig | 1 + src/browser/webapi/History.zig | 12 ++++----- src/browser/webapi/MessagePort.zig | 30 +++++++++++------------ src/browser/webapi/Window.zig | 19 ++++++-------- src/browser/webapi/net/XMLHttpRequest.zig | 12 ++++----- 8 files changed, 73 insertions(+), 65 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 17271635..573aa4f9 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -365,6 +365,29 @@ fn getFunction(handler: anytype, local: *const js.Local) ?js.Function { }; } +/// Check if there are any listeners for a direct dispatch (non-DOM target). +/// Use this to avoid creating an event when there are no listeners. +pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []const u8, handler: anytype) bool { + if (hasHandler(handler)) { + return true; + } + return self.lookup.get(.{ + .event_target = @intFromPtr(target), + .type_string = .wrap(typ), + }) != null; +} + +fn hasHandler(handler: anytype) bool { + const ti = @typeInfo(@TypeOf(handler)); + if (ti == .null) { + return false; + } + if (ti == .optional) { + return handler != null; + } + return true; +} + fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void { const ShadowRoot = @import("webapi/ShadowRoot.zig"); diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0cc7d8d9..014ebb62 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -791,24 +791,19 @@ fn _documentIsComplete(self: *Page) !void { try self.dispatchLoad(); // Dispatch window.load event. - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); - // This event is weird, it's dispatched directly on the window, but - // with the document as the target. - event._target = self.document.asEventTarget(); - try self._event_manager.dispatchDirect( - self.window.asEventTarget(), - event, - self.window._on_load, - .{ .inject_target = false, .context = "page load" }, - ); + const window_target = self.window.asEventTarget(); + if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) { + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); + // This event is weird, it's dispatched directly on the window, but + // with the document as the target. + event._target = self.document.asEventTarget(); + try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" }); + } - const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent(); - try self._event_manager.dispatchDirect( - self.window.asEventTarget(), - pageshow_event, - self.window._on_pageshow, - .{ .context = "page show" }, - ); + if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) { + const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent(); + try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" }); + } self.notifyParentLoadComplete(); } diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index de685efc..24bd2a7e 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -76,13 +76,11 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void { } // Dispatch abort event - const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page); - try page._event_manager.dispatchDirect( - self.asEventTarget(), - event, - self._on_abort, - .{ .context = "abort signal" }, - ); + const target = self.asEventTarget(); + if (page._event_manager.hasDirectListeners(target, "abort", self._on_abort)) { + const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page); + try page._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = "abort signal" }); + } } // Static method to create an already-aborted signal diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 3fd78f8b..31f472c3 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -138,6 +138,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { .screen => writer.writeAll(""), .screen_orientation => writer.writeAll(""), .visual_viewport => writer.writeAll(""), + .file_reader => writer.writeAll(""), }; } diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index b8819708..12336f2c 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -79,13 +79,11 @@ fn goInner(delta: i32, page: *Page) !void { if (entry._url) |url| { if (try page.isSameOrigin(url)) { - const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent(); - try page._event_manager.dispatchDirect( - page.window.asEventTarget(), - event, - page.window._on_popstate, - .{ .context = "Pop State" }, - ); + const target = page.window.asEventTarget(); + if (page._event_manager.hasDirectListeners(target, "popstate", page.window._on_popstate)) { + const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent(); + try page._event_manager.dispatchDirect(target, event, page.window._on_popstate, .{ .context = "Pop State" }); + } } } diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index dfe031f7..51d208b0 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -122,23 +122,21 @@ const PostMessageCallback = struct { return null; } - const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{ - .data = self.message, - .origin = "", - .source = null, - }, page) catch |err| { - log.err(.dom, "MessagePort.postMessage", .{ .err = err }); - return null; - }).asEvent(); + const target = self.port.asEventTarget(); + if (page._event_manager.hasDirectListeners(target, "message", self.port._on_message)) { + const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{ + .data = self.message, + .origin = "", + .source = null, + }, page) catch |err| { + log.err(.dom, "MessagePort.postMessage", .{ .err = err }); + return null; + }).asEvent(); - page._event_manager.dispatchDirect( - self.port.asEventTarget(), - event, - self.port._on_message, - .{ .context = "MessagePort message" }, - ) catch |err| { - log.err(.dom, "MessagePort.postMessage", .{ .err = err }); - }; + page._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| { + log.err(.dom, "MessagePort.postMessage", .{ .err = err }); + }; + } return null; } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 4d445e07..40265bb5 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -551,17 +551,14 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, }); } - const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{ - .reason = if (rejection.reason()) |r| try r.temp() else null, - .promise = try rejection.promise().temp(), - }, page)).asEvent(); - - try page._event_manager.dispatchDirect( - self.asEventTarget(), - event, - self._on_unhandled_rejection, - .{ .inject_target = true, .context = "window.unhandledrejection" }, - ); + const target = self.asEventTarget(); + if (page._event_manager.hasDirectListeners(target, "unhandledrejection", self._on_unhandled_rejection)) { + const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{ + .reason = if (rejection.reason()) |r| try r.temp() else null, + .promise = try rejection.promise().temp(), + }, page)).asEvent(); + try page._event_manager.dispatchDirect(target, event, self._on_unhandled_rejection, .{ .context = "window.unhandledrejection" }); + } } const ScheduleOpts = struct { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index c1380908..bf442c13 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -508,13 +508,11 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void { self._ready_state = state; - const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page); - try page._event_manager.dispatchDirect( - self.asEventTarget(), - event, - self._on_ready_state_change, - .{ .context = "XHR state change" }, - ); + const target = self.asEventTarget(); + if (page._event_manager.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) { + const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page); + try page._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = "XHR state change" }); + } } fn parseMethod(method: []const u8) !Http.Method {