Two issues prevented slotted elements from correctly inheriting
styles from their assigned slot:
1. Element::element_to_inherit_style_from() was skipping the slot
element and returning the shadow host instead. This meant slotted
elements inherited from the host, completely ignoring any styles
on the slot itself.
2. When a slot element's style changed during the style tree walk,
its assigned (slotted) nodes were never marked for recomputation.
The tree walk follows the DOM tree, but slotted elements are DOM
children of the shadow host, not the slot, so they were missed.
Fix (1) by returning the slot directly as the inheritance parent.
Fix (2) by marking assigned nodes dirty in update_style_recursively
when a slot's style changes.
Before calling update_style() for a getComputedStyle property access,
we now check whether the target element actually needs a style update
by walking the flat tree ancestor chain. If neither the element nor any
of its ancestors have dirty style bits, and there are no document-level
reasons to recalculate style, we skip the update_style() call entirely.
We walk the flat tree (not the DOM tree) because style inheritance
follows slot assignment -- slotted elements inherit from their assigned
slot, not their DOM parent.
This avoids unnecessary work when scripts access computed style
properties on elements whose styles are already up-to-date, which is a
common pattern on the web.
Previously, `<link rel=stylesheet>` would delay the load event until its
style sheet loaded, but not care about its subresources. `<style>`
would not delay the load event at all. Instead, each `@import` would
delay the load event.
Now, both `<style>` and `<link>` delay the load event until their style
sheet and its critical subresources have loaded or failed. This means
that CSSImportRules no longer need to delay the load event themselves,
because they do so implicitly as a critical subresource of their parent
style sheet.
This doesn't directly affect behavior, but means that any other critical
style resources we add will automatically delay the load event.
One wrinkle here is that the spec for the `<link>` element requires that
we wait for the style sheet's critical subresources *before* we create
a CSSStyleSheet, which means we don't yet know what those are.
https://html.spec.whatwg.org/multipage/semantics.html#fetching-and-processing-a-resource-from-a-link-element:critical-subresources
For now we simply ignore this, as we did before. That means we continue
to not delay the `<link>`'s load event.
Previously, we fired the load event immediately, without waiting for
anything. This was good for not timing out, but bad for anything that
wanted to wait for the load to complete.
CSSStyleSheet now maintains a list of critical subresources, and waits
for all of them to complete before it then tells its owner that it is
ready. "Complete" here means the network request completed with or
without an error. This is done by having those subresources (just
`@import` for now) notify their style sheet when they complete. This
then propagates up as an `@import` tells its style sheet, which then
would tell its parent `@import` if it had one.
There are other subresources we should wait for (specifically fonts and
background images) but this commit just adds `@import` as a first step.
Remove unused/redundant includes from Element.h:
- AK/IterationDecision.h (redundant)
- ARIA/AttributeNames.h (redundant via ARIAMixin.h)
- CSS/CascadedProperties.h (redundant via PseudoElement.h)
- CSS/StylePropertyMapReadOnly.h (pointer types only)
- HTML/LazyLoadingElement.h (unused in header)
Extract IntersectionObserverRegistration struct from
IntersectionObserver.h into its own lightweight header.
This breaks the heavy transitive include chain through
IntersectionObserverEntry.h and Geometry/DOMRect.h that
was pulled into every file including Element.h.
Indirect recompilation impact reductions:
- IntersectionObserver.h: ~1387 -> ~27 files
- LazyLoadingElement.h: ~1387 -> ~1002 files
Remove includes from Node.h that are only needed for forward
declarations (AccessibilityTreeNode.h, XMLSerializer.h,
JsonObjectSerializer.h). Extract StyleInvalidationReason and
FragmentSerializationMode enums into standalone lightweight
headers so downstream headers (CSSStyleSheet.h, CSSStyleProperties.h,
HTMLParser.h) can include just the enum they need instead of all of
Node.h. Replace Node.h with forward declarations in headers that only
use Node by pointer/reference.
This breaks the circular dependency between Node.h and
AccessibilityTreeNode.h, reducing AccessibilityTreeNode.h's
recompilation footprint from ~1399 to ~25 files.
Remove 11 heavy includes from Document.h that were only needed for
pointer/reference types (already forward-declared in Forward.h), and
extract the nested ViewportClient interface to a standalone header.
This reduces Document.h's recompilation cascade from ~1228 files to
~717 files (42% reduction). Headers like BrowsingContext.h that were
previously transitively included see even larger improvements (from
~1228 down to ~73 dependents).
Previously only input elements were matched. Add placeholder_value()
to HTMLTextAreaElement mirroring the HTMLInputElement API and update
both selector matching code paths to handle textarea.
If we want to test whether or not we're drawing the caret, we need to
prevent it from blinking or otherwise all tests we're going to write
that look at the display list will turn out to be flaky.
When the mouse is dragged from inside a scrollable container to outside
of it, we now automatically scroll the container so the selection can be
extended. Scroll speed scales with the distance past the scrollport
edge, capped at a maximum. Edges close to the viewport boundary get a
wider activation zone so the speed ramp works predictably even when the
mouse has limited room to move.
The logic is encapsulated in AutoScrollHandler, which EventHandler
creates lazily on mouse selection start.
When editing or changing the selection inside an <input> or <textarea>,
we should scroll the container so the cursor is always visible. Note
that currently the cursor might still become invisible at the end of the
container since we do not reserve enough space for it to be made
visible.
Replace the direct #include of StyleInvalidation.h in Element.h with a
forward declaration in Forward.h. Element.h only uses the type in
function declarations, so the complete type is not needed.
This reduces the recompilation impact of modifying StyleInvalidation.h
from ~1380 files to ~4 files, since Element.h is transitively included
by nearly every HTML and SVG element header.
Add a document-level boolean flag that tracks whether any :has()
invalidations have been scheduled. This avoids iterating over all
shadow roots just to check is_empty() on each style scope when no
:has() invalidations are pending, which is the common case during
scrolling on complex pages like Reddit.
Results in ~10% reduction of is_empty() calls in profiles when
scrolling on Reddit.
Replace AK::Function parameter with a template parameter so the
compiler can inline the lambda at each call site. This eliminates
type-erasure overhead (vtable indirection + ScopeGuard) that was
showing up in profiles during Reddit scrolling, where this function
is called repeatedly from update_style() for every shadow root on
every style update.
HTMLImageElement's "update the image data" algorithm checks
is_fully_active() at the start, but its async continuations
(microtasks, element tasks, batching dispatcher callbacks) skip
this check. When an iframe is removed or navigated, these
callbacks fire on an inactive document, causing crashes.
Fix this with two changes:
1) Add is_fully_active() bail-out checks at all async callback
entry points in HTMLImageElement. Each bail-out also clears
the DocumentLoadEventDelayer to prevent blocking the parent
document's load event forever.
2) Create the DocumentObserver eagerly in initialize() (like
HTMLMediaElement) with a document_became_inactive callback
that clears the load event delayer and stops the animation
timer. Fire document_became_inactive from Document::destroy()
in addition to did_stop_being_active_document_in_navigable(),
since iframe removal takes a different path than navigation.
A guard flag prevents duplicate firing.
When unload_a_document_and_its_descendants() iterates all_navigables()
to find descendant navigables, skip any that have been destroyed.
When an iframe is removed, destroy_the_child_navigable() marks its
navigable as destroyed synchronously, but removal from all_navigables()
happens later in an async callback from destroy_a_document_and_its_
descendants(). If we count these destroyed navigables as descendants and
queue unload tasks for them, Document::destroy() may run during the
subsequent spin_until and remove those tasks (since it clears all tasks
for its document). This causes number_unloaded to never reach
unloaded_documents_count, hanging the event loop permanently.
This was the root cause of intermittent hangs when running crash tests
in batch mode: the previous test's iframe cleanup would leave destroyed
navigables in all_navigables(), and the about:blank navigation between
tests would get stuck in the unload spin_until.
These attributes are consumed during layout in SVGFormattingContext to
compute the viewbox transform. They don't affect the layout tree
structure, so a layout-only invalidation is sufficient instead of a
full layout tree rebuild.
Merge the three consecutive for_each_in_inclusive_subtree traversals
into a single preorder walk. All three operations only depend on
ancestor state which is satisfied before descendants are visited in
preorder traversal.
Previously, any SVG geometry attribute change would mark the entire
document layout tree as dirty, triggering a full layout pass even though
only the SVG subtree was affected. This made SVG geometry animations
unnecessarily expensive.
Fix this by stopping `needs_layout_update` propagation at the SVGSVGBox
boundary and tracking dirty SVG roots separately on the Document. When
`update_layout()` finds that only SVG roots need relayout (and the
document layout root is clean), it runs SVGFormattingContext on each
dirty SVG root in a fresh LayoutState and commits the results directly,
bypassing the full document layout pass entirely.
This results in a substantial performance improvement on pages with
animated SVGs, such as https://www.cloudflare.com/,
https://www.duolingo.com/, and our GC graph explorer page.
This extends the null navigable check added in commit
b118c99c27 to include all ancestor and
descendant list lookups. Fixes a crash in the following WPT test:
/cookies/schemeful-same-site/schemeful-navigation.tentative.html
...and WorkerEnvironmentSettingsObject.h
These headers only use Document via forward-declarable references and
smart pointers, so the full include is unnecessary.
`SlottableMixin::assigned_slot()` was using `as<DOM::Node>(*this)` to
get a `Node` reference. Since `SlottableMixin` has no inheritance
relationship with `Node`, `as_if<>` can't use `static_cast` and falls
through to `dynamic_cast`, which is expensive. Replace this with a
virtual `slottable_as_node()` accessor overridden in `Element` and
`Text`.
This showed up as hot in profiles when loading the GC heap explorer
page.
The SVG `transform` attribute is stored on the DOM element and read
directly during layout by
`SVGFormattingContext::layout_graphics_element()`. Since changing the
transform doesn't affect which DOM nodes produce layout boxes or how
they're structured, we only need to re-run layout on the existing tree
instead of rebuild it from scratch.
When triple clicking on text, we should select the entire paragraph, or
entire line in <input>s and <textarea>s. If the mouse button is held
down and the user starts dragging, the selection expands with additional
paragraphs or lines.
This expands on the work of Kai Wildberger (PR #7681) but was adjusted
for the work that happened previously to support double click + drag
moves and includes triple click support for our Qt UI.
Co-authored-by: Kai Wildberger <kiawildberger@gmail.com>
We weren't doing this before either, but through a slightly sneaky
mechanism: we had overridden Cell::initialize() in
ShadowRealmGlobalScope as a no-op.
Instead of that, do the same thing Window and Worker globals do and
make all of the globals that inherit UniversalGlobalScopeMixin opt
out of WEB_SET_PROTOTYPE_FOR_INTERFACE in EventTarget::initialize().
Node::inserted() sets needs_style_update, and Node::removed_from()
clears the layout node and paintable pointers. Forgetting to call
either base implementation from a derived class would be a bug.
The base implementation handles critical bookkeeping like element
ID/name registration and slot assignment. Forgetting to call it
from a derived class would be a correctness bug.
`Layout::NodeWithStyle::computed_values()` actually holds used values
which may not be the same as computed values e.g. if they have been
modified by `Document::propagate_overflow_to_viewport()`
This patch introduces a cookie cache in the WebContent process to reduce
blocking IPC calls when JS accesses document.cookie. The UI process now
maintains a cookie version counter per-domain in shared memory. When JS
reads document.cookie, we check whether we have a valid cached cookie by
comparing the current shared version to the last used version. If they
match, the cached cookie is returned without IPC.
This optimization is based on Chromium's shared versioning, in which it
was observed that 87% of document.cookie accesses were redundant. See:
https://blog.chromium.org/2024/06/introducing-shared-memory-versioning-to.html
Note that this cache only supports document.cookie, not HTTP Cookie
headers. HTTP cookies are attached to requests with varying URLs and
paths. The cookies that match the document URL might not match the
request URL, which we wouldn't know from WebContent. So attaching the
cached document cookie would be incorrect.
On https://twinings.co.uk, we see approximately 600 document.cookie
requests while the page loads. This patch reduces the time spent in
the document.cookie getter from ~45ms to 2-3ms.
Every user of this actually wants an ancestor in the flat tree - taking
things like `<slot>` into account. So rename it and adjust its behavior
to use that.
Various parts of the spec ask us to walk the flat tree. In most cases
that's the same as getting its parent or shadow host, but `<slot>` in
particular breaks this rule, as a slotted element's flat tree parent is
the slot.
When `elementFromPoint()` or `elementsFromPoint()` returns an element
that is inside a UA internal shadow root, we now return the shadow host
for that element.
Instead of doing a full document style invalidation when a stylesheet is
dynamically added, we now analyze the new sheet's selectors to determine
which elements could potentially be affected, and only invalidate those.
This works by building an InvalidationSet from the rightmost compound
selector (the "subject") of each rule in the new stylesheet, extracting
class, ID, tag name, attribute, and pseudo-class features. We then walk
the DOM tree and only mark elements matching those features as needing a
style update.
If any selector has a rightmost compound that is purely universal (no
identifying features), or uses a pseudo-class not supported by the
invalidation set matching logic, we fall back to full invalidation.