LibWeb: Skip event dispatching work if there are no relevant listeners

We were going through path building, retargeting, capturing/bubbling
phases etc. for each event that we were dispatching. We now optimize for
the case where there are no listeners for the event type on the target
node nor its ancestors by skipping all that work.

This decimates methods that depend on event dispatching like
`Document::set_hovered_node()` when there are no relevant listeners,
which reduced its runtime by 99% (42.5ms -> 0.5ms) on my machine for
sites like https://wordsalad.online/.
This commit is contained in:
Jelle Raaijmakers 2026-03-10 11:01:33 +01:00 committed by Alexander Kalenik
parent 948bb4f45a
commit 57c9bb5caa
Notes: github-actions[bot] 2026-03-10 15:59:09 +00:00
3 changed files with 27 additions and 3 deletions

View file

@ -214,8 +214,18 @@ bool EventDispatcher::dispatch(GC::Ref<EventTarget> target, Event& event, bool l
// 5. Let clearTargets be false.
bool clear_targets = false;
// OPTIMIZATION: Only dispatch events if there is at least one listener on the node or its ancestors. Activation
// events are always dispatched. This saves us from going through the path building and dispatch
// phases for events that will be dropped on the floor anyway.
bool is_activation_event = is<UIEvents::MouseEvent>(event) && event.type() == HTML::EventNames::click;
bool should_dispatch = is_activation_event;
if (!should_dispatch) {
auto const* node = as_if<Node>(*target);
should_dispatch = !node || node->has_inclusive_ancestor_with_event_listener(event.type());
}
// 6. If target is not relatedTarget or target is events relatedTarget, then:
if (related_target != target || event.related_target() == target) {
if (should_dispatch && (related_target != target || event.related_target() == target)) {
// 1. Let touchTargets be a new list.
Event::TouchTargetList touch_targets;
@ -227,8 +237,9 @@ bool EventDispatcher::dispatch(GC::Ref<EventTarget> target, Event& event, bool l
// 3. Append to an event path with event, target, targetOverride, relatedTarget, touchTargets, and false.
event.append_to_path(*target, target_override, related_target, touch_targets, false);
// 4. Let isActivationEvent be true, if event is a MouseEvent object and events type attribute is "click"; otherwise false.
bool is_activation_event = is<UIEvents::MouseEvent>(event) && event.type() == HTML::EventNames::click;
// 4. Let isActivationEvent be true, if event is a MouseEvent object and events type attribute is "click";
// otherwise false.
// NB: Step 4 is executed above as part of an optimization.
// 5. If isActivationEvent is true and target has activation behavior, then set activationTarget to target.
if (is_activation_event && target->has_activation_behavior())

View file

@ -60,6 +60,7 @@
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Scripting/SimilarOriginWindowAgent.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/XMLSerializer.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Layout/Node.h>
@ -3406,6 +3407,17 @@ bool Node::has_inclusive_ancestor_with_display_none()
return false;
}
bool Node::has_inclusive_ancestor_with_event_listener(FlyString const& type) const
{
for (auto const* ancestor = this; ancestor; ancestor = ancestor->parent()) {
if (ancestor->has_event_listener(type))
return true;
}
if (auto window = document().window())
return window->has_event_listener(type);
return false;
}
GC::Ptr<ShadowRoot> Node::containing_shadow_root()
{
if (auto* shadow_root = as_if<ShadowRoot>(*this))

View file

@ -454,6 +454,7 @@ public:
bool is_inert() const;
bool has_inclusive_ancestor_with_display_none();
bool has_inclusive_ancestor_with_event_listener(FlyString const& type) const;
GC::Ptr<ShadowRoot> containing_shadow_root();
GC::Ptr<ShadowRoot const> containing_shadow_root() const