From cd0705334be55faedcedaec78fdb541d85bdd059 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 22 Jan 2026 15:09:56 +0100 Subject: [PATCH] LibWeb: Skip effect contexts for off-screen display list commands When executing display list commands, check if commands with effect contexts (opacity, filters, blend modes) are outside the viewport before applying the effect. Since effects don't affect clip state, would_be_fully_clipped_by_painter() returns the same result before and after applying effects. This avoids expensive saveLayer/restore cycles for off-screen commands with effects like blur, which is particularly beneficial for pages with many blurred decorative images (e.g., Discord's landing page has 70+ blurred star images). The optimization only applies when switching to a new effect context, not for consecutive commands with the same context, to preserve correct blend mode compositing behavior. --- .../LibWeb/Painting/AccumulatedVisualContext.h | 2 ++ Libraries/LibWeb/Painting/DisplayList.cpp | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h index 9215c8959b..e191fa7f29 100644 --- a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h +++ b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h @@ -86,6 +86,8 @@ public: VisualContextData const& data() const { return m_data; } RefPtr parent() const { return m_parent; } + bool is_effect() const { return m_data.has(); } + size_t depth() const { return m_depth; } size_t id() const { return m_id; } diff --git a/Libraries/LibWeb/Painting/DisplayList.cpp b/Libraries/LibWeb/Painting/DisplayList.cpp index bfa1cd79c6..4d27caeeb6 100644 --- a/Libraries/LibWeb/Painting/DisplayList.cpp +++ b/Libraries/LibWeb/Painting/DisplayList.cpp @@ -168,6 +168,21 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps for (size_t command_index = 0; command_index < commands.size(); command_index++) { auto const& [context, command] = commands[command_index]; + auto bounding_rect = command_bounding_rectangle(command); + + // OPTIMIZATION: If the leaf context is an effect and we're switching to a new context, + // check culling before applying it. Effects (opacity, filters, blend modes) don't affect + // clip state, so would_be_fully_clipped_by_painter() returns the same result before and after + // applying effects. + // This avoids expensive saveLayer/restore cycles for off-screen elements with effects like blur. + // NOTE: We must not do this for consecutive commands with the same context, as that would incorrectly restore + // and re-apply the effect layer, breaking blend mode compositing. + if (context && applied_context != context && context->is_effect() && bounding_rect.has_value()) { + switch_to_context(context->parent()); + if (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect)) + continue; + } + switch_to_context(context); if (command.has()) { @@ -185,7 +200,6 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps continue; } - auto bounding_rect = command_bounding_rectangle(command); if (bounding_rect.has_value() && (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))) { // Any clip or mask that's located outside of the visible region is equivalent to a simple clip-rect, // so replace it with one to avoid doing unnecessary work.