#7257 · Plugin selector ancestor filtering
Verdict: REPRODUCED · Root-cause confidence: high. This verdict covers isolated selector overhead on trusted main, not the reported desktop interaction timings.
Trusted base: 55135d1e5045d6c5d3c24f5a4d5556ed3e11cb00.
1. TL;DR
Plugin utility styles impose substantial work even on inserted host elements that are outside every plugin root. Two clean source checkouts generate CSS that takes about 24 ms to style and lay out a synthetic subtree; equivalent separately scoped rules take about 4 ms. The matching roots themselves are not the problem: combining alternatives inside an ancestor :where() prevents Chromium's ancestor filter from extracting a required attribute. Requiring the root-marker attribute is faster but is not behavior-preserving: a built-in toast uses only the named attribute. Separating entire rules by root alternative preserves that case, anonymous portals, zero scope specificity, and nested selector-list specificity.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Default plugin utilities have costly nonmatching ancestor selectors. | Verified | Trusted compiler output: current medians 24.2 and 24.1 ms; separated rules 4.1 ms in both clean runs. |
| A common required root attribute improves rejection. | Verified in the isolated fixture | Marker-required median 2.5 ms in both runs; same declarations and subtree. |
| Every named root already has a root marker. | Refuted | Built-in StarToast lacks the marker; browser checks show both self and descendant styles disappear for named-only roots with the marker-required form. |
| Desktop thread switching and sidebar toggling improve by the reported amounts. | Unverified | No macOS Electron UI interaction was measured. Headless synthetic timings must not be substituted for end-to-end measurements. |
| Other layout/invalidation causes are excluded in the full app. | Unverified | This fixture isolates sheet matching; it does not reconstruct the application's full layout or invalidation behavior. |
3. Environment
- Source: public get-bb/bb main at the full base commit above; two separate temporary worktrees, each initially clean.
- Build host: Linux x86_64, Node 22.19.0, pnpm 9.15.0.
- Browser: Chrome 149.0.7827.55, revision 3188f8a607ae7e067593be8aab7f02d2451fec07, headless Linux, document visibility visible.
- Browser Automation plugin controlled fresh disposable browser sessions. Each checkout generated its own fixture; no user's BB instance, account data, providers, or network-facing app plugins were used.
- No application server/data directory was needed. A discarded local HTTP attempt used loopback port 18757; actual browser runs loaded transferred JSON fixtures via file URLs, using no HTTP port.
4. Minimal reproduction
- Create two clean checkouts at the trusted commit. In each, install and build using the repository toolchain:
git clone https://github.com/get-bb/bb.git bb-repro cd bb-repro git checkout 55135d1e5045d6c5d3c24f5a4d5556ed3e11cb00 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build --filter=@get-bb/plugin-sdk pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/plugin-build -- src/scope-plugin-utilities.test.ts
The first broad build was attempted before SDK build completion and hit missing SDK dist entries. Building the SDK first resolved this ordering issue; both final broad builds passed (73 tasks). - Save the generator below as
/tmp/generate.mjs, then run from each checkout:node --import tsx /tmp/generate.mjs "$PWD" /tmp/selector-fixture
It emits 18 synthetic sheets through the actual source compiler, with 96 utilities each. Thefixedvariant appears only when testing the patch. - Save the measurement script below as
/tmp/measure.js. Open a fresh Browser Automation headless session on the machine containing/tmp/selector-fixture/fixtures.json. Substitute that machine's ID and the returned session ID:bb machine list --json bb browser-automation open --backend local --headless --machine HOST_ID --json bb browser-automation run SESSION_ID --script 'const page = await browser.getPage("main"); await page.goto("file:///tmp/selector-fixture/fixtures.json");' --json bb browser-automation run SESSION_ID --script-file /tmp/measure.js --script-host HOST_ID --timeout-ms 120000 --json bb browser-automation close SESSION_ID --jsonIf the source checkout is on another machine, transfer both the JSON and measurement script withbb file write --stdin --host HOST_IDfirst. The browser host and build host need not share a filesystem. Repeat from a new checkout and new empty browser session. - The script clones 961 elements below a container ancestor. Eight classes on each leaf collide with plugin utilities but there is no plugin ancestor. Each round rotates the variant order, replaces the sheets before timing, then times insertion plus a forced geometry read. Five warmup rounds are discarded and 19 are retained. This is combined style/layout wall time, not selector-only trace time.
Expected: nonmatching plugin sheets add little ancestor-walking work. Actual: default generated selectors cost roughly six times as much as the compatibility-preserving separated form.
| Variant | First run median ms | Second clean run median ms |
|---|---|---|
| current | 24.2 | 24.1 |
| markerRequired | 2.5 | 2.5 |
| owned | 3.5 | 3.5 |
| separated | 4.1 | 4.1 |
| disabled | 1.6 | 1.6 |
Generator (complete)
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
const [checkout, destination] = process.argv.slice(2);
if (!checkout || !destination) throw new Error("Pass checkout and output directory");
const { pluginScopeRoots, scopePluginUtilities, splitPluginUtilityScopes } = await import(
pathToFileURL(resolve(checkout, "packages/plugin-build/src/scope-plugin-utilities.ts"))
);
const utilities = `@layer utilities{${Array.from({ length: 96 }, (_, index) => `.probe-${index}{--probe-${index}:1}`).join("")}}`;
const variants = { current: [], markerRequired: [], owned: [], separated: [], disabled: [] };
if (splitPluginUtilityScopes) variants.fixed = [];
for (let index = 0; index < 18; index += 1) {
const identifier = `fixture-${index}`;
const named = `[data-bb-plugin="${identifier}"]`;
const anonymous = "[data-bb-plugin-root]:not([data-bb-plugin])";
variants.current.push(scopePluginUtilities(utilities, pluginScopeRoots(identifier)));
if (splitPluginUtilityScopes) {
variants.fixed.push(splitPluginUtilityScopes(variants.current.at(-1)));
}
variants.markerRequired.push(scopePluginUtilities(utilities, `[data-bb-plugin-root]:is(${named},:not([data-bb-plugin]))`));
variants.owned.push(scopePluginUtilities(utilities, named));
variants.separated.push(scopePluginUtilities(utilities, named) + scopePluginUtilities(utilities, anonymous));
}
await mkdir(destination, { recursive: true });
await writeFile(resolve(destination, "fixtures.json"), JSON.stringify(variants));
await writeFile(resolve(destination, "index.html"), `<!doctype html><meta charset="utf-8"><title>Issue 7257 isolated selector reproduction</title><h1>Isolated selector reproduction</h1><p>18 synthetic sheets emitted by the trusted plugin utility compiler. No application data.</p><div id="host" style="container-type:inline-size"></div>`);
console.log(JSON.stringify({ sheetCount: 18, utilitiesPerSheet: 96, output: "fixtures.json" }));
Measurement script (complete)
const page = await browser.getPage("main");
const result = await page.evaluate(async () => {
const fixtures = JSON.parse(document.body.innerText);
document.body.innerHTML = '<h1>Isolated selector reproduction</h1><div id="host" style="container-type:inline-size"></div>';
const host = document.querySelector("#host");
const sheets = [];
const sample = document.createElement("div");
for (let row = 0; row < 48; row += 1) {
let ancestor = sample;
for (let depth = 0; depth < 12; depth += 1) {
const branch = document.createElement("div");
ancestor.append(branch);
ancestor = branch;
}
for (let column = 0; column < 8; column += 1) {
const leaf = document.createElement("span");
leaf.className = Array.from({ length: 8 }, (_, offset) => `probe-${(row + column + offset) % 96}`).join(" ");
leaf.textContent = "fixture ";
ancestor.append(leaf);
}
}
const names = Object.keys(fixtures);
const samples = Object.fromEntries(names.map((name) => [name, []]));
function activate(name) {
sheets.forEach((sheet) => sheet.remove());
sheets.length = 0;
for (const css of fixtures[name]) {
const style = document.createElement("style");
style.textContent = css;
document.head.append(style);
sheets.push(style);
}
host.getBoundingClientRect();
}
for (let round = 0; round < 24; round += 1) {
for (let position = 0; position < names.length; position += 1) {
const name = names[(position + round) % names.length];
activate(name);
const cloned = sample.cloneNode(true);
const start = performance.now();
host.append(cloned);
cloned.getBoundingClientRect();
const elapsed = performance.now() - start;
cloned.remove();
host.getBoundingClientRect();
if (round >= 5) samples[name].push(elapsed);
}
}
const matching = {};
const roots = {
namedOnly: { "data-bb-plugin": "fixture-0" },
namedMarked: { "data-bb-plugin": "fixture-0", "data-bb-plugin-root": "" },
anonymous: { "data-bb-plugin-root": "" },
otherPlugin: { "data-bb-plugin": "other", "data-bb-plugin-root": "" },
};
for (const name of names) {
activate(name);
matching[name] = {};
for (const [rootName, attributes] of Object.entries(roots)) {
const root = document.createElement("div");
for (const [key, value] of Object.entries(attributes)) root.setAttribute(key, value);
root.className = "probe-0";
const child = document.createElement("span");
child.className = "probe-0";
root.append(child);
host.append(root);
matching[name][rootName] = {
self: getComputedStyle(root).getPropertyValue("--probe-0").trim(),
descendant: getComputedStyle(child).getPropertyValue("--probe-0").trim(),
};
root.remove();
}
}
sheets.forEach((sheet) => sheet.remove());
const medians = Object.fromEntries(Object.entries(samples).map(([name, values]) => {
const sorted = [...values].sort((left, right) => left - right);
return [name, sorted[Math.floor(sorted.length / 2)]];
}));
return {
userAgent: navigator.userAgent,
visibility: document.visibilityState,
nodeCount: sample.querySelectorAll("*").length + 1,
warmupRounds: 5,
measuredRounds: 19,
mediansMs: medians,
samplesMs: samples,
matching,
};
});
console.log(JSON.stringify(result, null, 2));
Matching contract
Computed --probe-0 is 1 on the root and descendant for current and separated forms, whether the root is named-only, named-plus-marker, or anonymous. An unrelated named plugin matches neither. Marker-required output loses both matches on a named-only root; owned-only output loses the anonymous matches.
5. Root cause
pluginScopeRoots returns a union of named roots and unnamed marker roots. scopePluginUtilities puts that union inside one zero-specificity :where(). scopeStatements emits descendant and self rules and preserves nested selector-list specificity. Common utility classes cause these nonmatching descendant rules to be considered on host elements.
The exact browser revision's ancestor identifier collector only traverses :where()/:is() when its selector list has one alternative. Its separate subject-filter implementation can intersect alternatives; that does not provide the missing ancestor attribute here. The timings and engine implementation support the reported ancestor-filter mechanism. No CSS selector-statistics trace was captured.
loadPluginAppBundle previously kept full-scope CSS unchanged when loading served assets, so already-built plugin sheets retained the slow form. Its owned-root negotiation removes the anonymous arm only for eligible bundles and capable clients; it does not repair full-scope requests.
Compatibility counterexample: StarToast sets a named attribute without a root marker. Requiring a marker therefore changes an existing rendered contract, not merely hypothetical third-party markup.
6. Proposed fix and tested patch
At asset loading, split compiler-generated utility rules into one complete rule per root alternative, keeping each :where() single-argument. Duplicate each original selector list and body as a unit rather than splitting list members into independently specific rules. Keep authored non-utility CSS, unmatched lists, declaration strings, and keyframes intact. Derive owned-root variants before splitting full-scope variants, then hash and compress the final served bytes. This preserves current roots without requiring a new marker contract, protocol, dependency, or rebuild of installed plugins.
The patch touches five existing files in the plugin CSS subsystem. The actual patched helper, not just the experimental separated control, was benchmarked: current 24.5 ms versus fixed 4.5 ms. The browser's four-root self/descendant matching matrix was identical before and after. Splitting duplicates declarations and can increase full-scope stylesheet size; normal asset compression remains in use. Exact UI latency benefits remain unmeasured.
7. Verification
The same agent repeated the reproduction from a second clean temporary worktree at 55135d1e5045d6c5d3c24f5a4d5556ed3e11cb00, with a separate frozen install/build, independently generated fixture bytes, and a new empty browser session. Both generated fixture files had the same SHA-256 (3d8f2203dc6584b6a57b8468f2e8d166af9e05ca1c103237d06288bf696fd7f5), and both runs showed the same slowdown and compatibility counterexample. The second compiler test execution bypassed Turbo's result cache and passed 33 tests. No claim of a separate reviewer or independent agent is made.
Report correction after checking source and browser behavior: named roots do not universally carry a marker. The safe patch uses separate alternatives instead of imposing that prerequisite.
Focused regression, before production changes
pnpm exec turbo run test --filter=@bb/server -- test/services/plugins/plugin-app-bundle.test.ts -t 'serves filterable utility scopes'
Exit: 1
Tests: 1 failed | 14 skipped
Expected: @layer utilities{:where([data-bb-plugin=scope]) .p-2{padding:.5rem}:where([data-bb-plugin-root]:not([data-bb-plugin])) .p-2{padding:.5rem}}[data-bb-plugin-root]:not([data-bb-plugin]) .authored{color:red}
Received: @layer utilities{:where([data-bb-plugin=scope],[data-bb-plugin-root]:not([data-bb-plugin])) .p-2{padding:.5rem}}[data-bb-plugin-root]:not([data-bb-plugin]) .authored{color:red}
The regression extends the existing real SQLite-backed asset-route scenario for all combinations of root eligibility and scope negotiation. It asserts literal served CSS, not an expected result generated by the transformer under test. Separate compiler tests protect quoting, conditional layers, nested specificity, idempotence, and preservation of non-generated CSS.
After the fix
pnpm exec turbo run test --filter=@bb/server -- test/services/plugins/plugin-app-bundle.test.ts 15 tests passed pnpm exec turbo run test --filter=@bb/plugin-build 266 tests passed; 1 skipped; 15 test files passed pnpm exec turbo run lint typecheck --filter=@bb/plugin-build --filter=@bb/server Both package typechecks passed; no package lint scripts were configured for these targets git diff --check Passed git diff --numstat origin/main 96 additions + 10 deletions = 106 changed text lines in 5 files; no binary changes
A browser cascade check additionally used a mixed-class/ID nested selector list with a later competing double-class rule. Current and patched output both computed the plugin value for named-only and anonymous roots and the host value for an unrelated plugin, on roots and descendants. This tests the reason for duplicating entire selector lists rather than fragmenting their nested specificity.
Full application/desktop smoke journeys were not run. The optional verification-map inventory check stopped on a pre-existing unmapped diagnostics CLI family; that unrelated documentation drift was not changed.
8. Related issues and PRs
The repository's existing owned-root optimization is distinct: this defect remains in full-scope CSS even without opting into narrower ownership. No linked open pull request existed in the issue timeline or open-PR search when the investigation and fix started. This report does not execute or validate any untrusted linked branch.
9. Appendix and trust boundary
All issue content was treated as untrusted hypotheses. No issue-provided command, patch, branch, script, or external link was executed. The reproduction and patch were written from trusted main source evidence. Public output includes no runtime credentials, user data, machine identifiers, or local checkout paths. Complete reproduction code is inline above; raw process logs and local artifacts remain outside published git content.
{
"firstRun": {
"current": 24.19999998807907,
"markerRequired": 2.5,
"owned": 3.5,
"separated": 4.099999964237213,
"disabled": 1.600000023841858
},
"secondCleanRun": {
"current": 24.100000023841858,
"markerRequired": 2.5,
"owned": 3.5,
"separated": 4.100000023841858,
"disabled": 1.600000023841858
},
"patchedRun": {
"current": 24.5,
"markerRequired": 2.699999988079071,
"owned": 3.800000011920929,
"separated": 4.399999976158142,
"disabled": 1.899999976158142,
"fixed": 4.5
},
"firstRunSamples": {
"current": [
24.400000035762787,
24.5,
24.899999976158142,
24.5,
24.099999964237213,
24.100000023841858,
24.100000023841858,
24.100000023841858,
24.100000023841858,
24.099999964237213,
24,
24.19999998807907,
24.80000001192093,
24.5,
24.099999964237213,
24.100000023841858,
24.19999998807907,
24.399999976158142,
24.399999976158142
],
"markerRequired": [
2.899999976158142,
2.799999952316284,
2.900000035762787,
2.900000035762787,
2.300000011920929,
2.399999976158142,
2.5,
2.299999952316284,
2.5,
2.5,
2.599999964237213,
2.399999976158142,
2.699999988079071,
4,
2.5,
2.5,
2.300000011920929,
2.5,
2.800000011920929
],
"owned": [
3.900000035762787,
3.899999976158142,
3.800000011920929,
3.899999976158142,
3.300000011920929,
3.5,
3.399999976158142,
3.5,
3.399999976158142,
3.5,
3.5,
3.5,
3.699999988079071,
4.300000011920929,
3.599999964237213,
3.600000023841858,
3.5,
3.5,
3.800000011920929
],
"separated": [
4.5,
4.5,
4.5,
4.599999964237213,
3.900000035762787,
3.899999976158142,
3.899999976158142,
7.599999964237213,
4,
4.099999964237213,
4.099999964237213,
4.100000023841858,
4.199999988079071,
4.399999976158142,
3.800000011920929,
4,
4,
4.099999964237213,
4.300000011920929
],
"disabled": [
2,
1.899999976158142,
1.899999976158142,
1.9000000357627869,
4.800000011920929,
1.399999976158142,
1.5999999642372131,
2,
1.600000023841858,
1.5,
1.5,
1.5,
1.899999976158142,
1.899999976158142,
1.5999999642372131,
1.600000023841858,
1.5,
1.5,
1.800000011920929
]
},
"secondRunSamples": {
"current": [
24.80000001192093,
24.5,
24.600000023841858,
24.5,
24.600000023841858,
24.69999998807907,
24.099999964237213,
24.099999964237213,
24,
24.099999964237213,
24.100000023841858,
27.899999976158142,
24.100000023841858,
24.100000023841858,
24,
24.099999964237213,
24.100000023841858,
24.399999976158142,
24.30000001192093
],
"markerRequired": [
2.800000011920929,
2.899999976158142,
2.900000035762787,
3,
2.900000035762787,
2.900000035762787,
2.900000035762787,
2.399999976158142,
2.399999976158142,
2.399999976158142,
2.400000035762787,
2.5,
2.400000035762787,
2.299999952316284,
2.399999976158142,
2.300000011920929,
2.399999976158142,
2.699999988079071,
2.600000023841858
],
"owned": [
3.799999952316284,
3.899999976158142,
4,
3.899999976158142,
4,
4,
3.799999952316284,
3.400000035762787,
3.399999976158142,
3.5,
3.5,
3.5,
4.100000023841858,
3.5,
3.5,
3.5,
3.5,
3.399999976158142,
3.800000011920929
],
"separated": [
4.5,
4.5,
4.5,
4.5,
4.599999964237213,
4.5,
8,
4,
4.100000023841858,
4.100000023841858,
4.100000023841858,
4,
3.800000011920929,
4,
4.100000023841858,
4.100000023841858,
4,
4,
4.300000011920929
],
"disabled": [
1.899999976158142,
2,
2,
1.9000000357627869,
1.9000000357627869,
2,
1.600000023841858,
1.5,
1.600000023841858,
1.5,
1.600000023841858,
1.5999999642372131,
1.5999999642372131,
1.5,
1.5,
1.5,
1.600000023841858,
1.5999999642372131,
1.9000000357627869
]
}
}