Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/context-interning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-selector": patch
---

**Fixed:** `Context.hover()`/`.active()`/`.focus()`/`.visit()` (the static factories) now return the same `Context` instance when called repeatedly for the same element, instead of constructing a fresh one every time. Since `alfa-cascade`/`alfa-style`'s caches are keyed by `Context` object identity, repeated single-element-context queries (e.g. a rule checking many candidate elements' style under one target's `:focus` context) previously missed those caches on every call. Measured on `sia-r65` (which does exactly this): ~131.4s → ~0.4s on a 10k-node real-world page, with identical pass/fail outcomes before and after.
25 changes: 21 additions & 4 deletions packages/alfa-selector/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Cache } from "@siteimprove/alfa-cache";
import type { Element } from "@siteimprove/alfa-dom";
import { Map } from "@siteimprove/alfa-map";

Expand Down Expand Up @@ -49,8 +50,18 @@ export class Context {
return this.addState(element, Context.State.Hover);
}

private static _hovered = Cache.empty<Element, Context>();

/**
* @remarks
* Interned per element: repeated calls for the same element return the
* same instance, so downstream `Cache<Context, _>` lookups (e.g. in
* `Cascade`/`Style`) can actually hit across separate call sites querying
* the same single-element context instead of always missing on a fresh
* object.
*/
public static hover(element: Element): Context {
return this.empty().hover(element);
return this._hovered.get(element, () => this.empty().hover(element));
}

public isHovered(element: Element): boolean {
Expand All @@ -61,8 +72,10 @@ export class Context {
return this.addState(element, Context.State.Active);
}

private static _activated = Cache.empty<Element, Context>();

public static active(element: Element): Context {
return this.empty().active(element);
return this._activated.get(element, () => this.empty().active(element));
}

public isActive(element: Element): boolean {
Expand All @@ -73,8 +86,10 @@ export class Context {
return this.addState(element, Context.State.Focus);
}

private static _focused = Cache.empty<Element, Context>();

public static focus(element: Element): Context {
return this.empty().focus(element);
return this._focused.get(element, () => this.empty().focus(element));
}

public isFocused(element: Element): boolean {
Expand All @@ -85,8 +100,10 @@ export class Context {
return this.addState(element, Context.State.Visited);
}

private static _visited = Cache.empty<Element, Context>();

public static visit(element: Element): Context {
return this.empty().visit(element);
return this._visited.get(element, () => this.empty().visit(element));
}

public isVisited(element: Element): boolean {
Expand Down