mirror of https://github.com/sveltejs/svelte
perf: optimize CSS selector pruning (#17846)
## Summary Reduces CSS selector pruning overhead by eliminating unnecessary allocations and redundant work in the hot path. **Changes:** - Replace `.slice()` + `.shift()`/`.pop()` in `apply_selector` with index-based `from`/`to` params — avoids O(n) array copies per recursive call - Merge `has_selectors`/`other_selectors` split in `relative_selector_might_apply_to_node` into a single pass — eliminates 2 temporary array allocations per call - Hoist `name.toLowerCase()` out of the inner loop in `attribute_matches` - Replace `value.split(/\s/).includes()` with `indexOf` + boundary checks in `test_attribute` for `~=` — avoids array allocation on every class match - Skip `name.replace()` regex when selector name has no backslash ## Benchmark Interleaved benchmark (5 rounds, alternating baseline/optimized): ``` --- Round 1 --- Baseline: min=59.58ms median=64.63ms Optimized: min=47.20ms median=54.35ms --- Round 2 --- Baseline: min=59.11ms median=64.90ms Optimized: min=48.36ms median=54.74ms --- Round 3 --- Baseline: min=58.38ms median=64.06ms Optimized: min=48.38ms median=53.83ms --- Round 4 --- Baseline: min=58.49ms median=63.99ms Optimized: min=48.45ms median=53.82ms --- Round 5 --- Baseline: min=58.40ms median=64.07ms Optimized: min=48.97ms median=54.64ms Best min: Before=58.38ms After=47.20ms Improvement=19.1% Best median: Before=63.99ms After=53.82ms Improvement=15.9% ``` CPU profile before → after: | Function | Before | After | |---|---|---| | `relative_selector_might_apply_to_node` | 14.3% | 5.2% | | `attribute_matches` | 4.0% | 3.3% | | `test_attribute` | 3.2% | <0.9% | ## Test plan - [x] All 196 CSS tests pass (180 samples + 16 parse) - [x] All 31 snapshot tests pass - [x] All 2380 runtime-runes tests pass - [x] All 3291 runtime-legacy tests pass - [x] All 145 compiler-errors tests pass - [x] All 326 validator tests pass - [x] Added `css-prune-edge-cases` test covering: `~=` word matching (substring vs whole word), deep combinator chains (4+ levels), `:has()` combined with class selectors, escaped selectors, `:is()`/`:where()`/`:not()` with combinators - [x] Edge case test passes on both baseline and optimized code 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/17842/head
parent
32111f9e84
commit
0965028d3b
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
perf: optimize CSS selector pruning
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
warnings: [
|
||||||
|
{
|
||||||
|
code: 'css_unused_selector',
|
||||||
|
message: 'Unused CSS selector ".foob"',
|
||||||
|
start: {
|
||||||
|
line: 64,
|
||||||
|
column: 1,
|
||||||
|
character: 1574
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line: 64,
|
||||||
|
column: 6,
|
||||||
|
character: 1579
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'css_unused_selector',
|
||||||
|
message: 'Unused CSS selector "main > article > div > section > span"',
|
||||||
|
start: {
|
||||||
|
line: 84,
|
||||||
|
column: 1,
|
||||||
|
character: 2196
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line: 84,
|
||||||
|
column: 38,
|
||||||
|
character: 2233
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'css_unused_selector',
|
||||||
|
message: 'Unused CSS selector "nav:has(button).primary"',
|
||||||
|
start: {
|
||||||
|
line: 95,
|
||||||
|
column: 1,
|
||||||
|
character: 2560
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line: 95,
|
||||||
|
column: 24,
|
||||||
|
character: 2583
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
/* === ~= word matching === */
|
||||||
|
|
||||||
|
/* Should match: "foo" is a whole word in class="foo bar" */
|
||||||
|
.foo.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "bar" is a whole word in class="foo bar" */
|
||||||
|
.bar.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "foobar" is the whole class value */
|
||||||
|
.foobar.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "bar-foo" is a whole word (hyphen not whitespace) */
|
||||||
|
.bar-foo.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "baz" is a whole word in class="bar-foo baz" */
|
||||||
|
.baz.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: "foob" is not a word in any element's class */
|
||||||
|
/* (unused) .foob { color: red; }*/
|
||||||
|
|
||||||
|
/* Should NOT match: "afoo" is a word but "foo-x" is not "foo" */
|
||||||
|
[class~="foo-x"].svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Attribute selector with ~= operator directly */
|
||||||
|
[class~="afoo"].svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* === Deep combinator chains (4+ levels) === */
|
||||||
|
|
||||||
|
/* Should match: exact chain main > article > section > div > span */
|
||||||
|
main.svelte-xyz > article:where(.svelte-xyz) > section:where(.svelte-xyz) > div:where(.svelte-xyz) > span:where(.svelte-xyz) { color: green; }
|
||||||
|
|
||||||
|
/* Should match: descendant chain */
|
||||||
|
main.svelte-xyz article:where(.svelte-xyz) section:where(.svelte-xyz) div:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
|
||||||
|
|
||||||
|
/* Should match: mixed combinators */
|
||||||
|
main.svelte-xyz > article:where(.svelte-xyz) section:where(.svelte-xyz) > div:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: wrong nesting order */
|
||||||
|
/* (unused) main > article > div > section > span { color: red; }*/
|
||||||
|
|
||||||
|
/* === :has() combined with other selectors === */
|
||||||
|
|
||||||
|
/* Should match: nav.primary has <a> descendant */
|
||||||
|
nav:has(a:where(.svelte-xyz)).primary.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should match: nav.secondary has <button> descendant */
|
||||||
|
nav:has(button:where(.svelte-xyz)).secondary.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: nav.primary doesn't have <button> */
|
||||||
|
/* (unused) nav:has(button).primary { color: red; }*/
|
||||||
|
|
||||||
|
/* Multiple :has() on same element */
|
||||||
|
main.svelte-xyz:has(article:where(.svelte-xyz)):has(span:where(.svelte-xyz)) { color: green; }
|
||||||
|
|
||||||
|
/* :has() with child combinator */
|
||||||
|
main.svelte-xyz:has(> article:where(.svelte-xyz)) { color: green; }
|
||||||
|
|
||||||
|
/* === Escaped selectors === */
|
||||||
|
.a\-b.svelte-xyz { color: green; }
|
||||||
|
|
||||||
|
/* === :is()/:where()/:not() with deep selectors === */
|
||||||
|
|
||||||
|
/* :is() with matching selector */
|
||||||
|
header.svelte-xyz :is(h1:where(.svelte-xyz)) { color: green; }
|
||||||
|
|
||||||
|
/* :where() with matching selector */
|
||||||
|
ul.svelte-xyz :where(li:where(.svelte-xyz)) { color: green; }
|
||||||
|
|
||||||
|
/* :not() — should match span since it's not a div */
|
||||||
|
span.svelte-xyz:not(div) { color: green; }
|
||||||
|
|
||||||
|
/* :is() with deep combinator */
|
||||||
|
ul.svelte-xyz :is(li:where(.svelte-xyz) > span:where(.svelte-xyz)) { color: green; }
|
||||||
|
|
||||||
|
/* :not() with class — p.a-b is :not(.unused) */
|
||||||
|
p.svelte-xyz:not(.unused) { color: green; }
|
||||||
|
|
||||||
|
/* Complex: :has() + :is() */
|
||||||
|
ul.svelte-xyz:has(li:where(.svelte-xyz)) :is(span:where(.svelte-xyz)) { color: green; }
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
<!-- Edge cases for CSS pruning optimizations:
|
||||||
|
1. ~= word matching (indexOf vs split)
|
||||||
|
2. Deep combinator chains (index-based apply_selector)
|
||||||
|
3. :has() combined with other selectors (single-pass handling)
|
||||||
|
4. Escaped selectors (backslash skip optimization)
|
||||||
|
5. :is()/:where()/:not() with deep selectors
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- ~= word matching edge cases -->
|
||||||
|
<div class="foo bar">word match</div>
|
||||||
|
<div class="foobar">substring only</div>
|
||||||
|
<div class="bar-foo baz">hyphen separated</div>
|
||||||
|
<div class="afoo foo-x">prefix substring</div>
|
||||||
|
|
||||||
|
<!-- Deep combinator chains -->
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<section>
|
||||||
|
<div>
|
||||||
|
<span class="deep">deep</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- :has() with class selectors -->
|
||||||
|
<nav class="primary">
|
||||||
|
<a href="/">link</a>
|
||||||
|
</nav>
|
||||||
|
<nav class="secondary">
|
||||||
|
<button>action</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Escaped selectors -->
|
||||||
|
<p class="a-b">escaped</p>
|
||||||
|
|
||||||
|
<!-- :is()/:where()/:not() with combinators -->
|
||||||
|
<header>
|
||||||
|
<h1>title</h1>
|
||||||
|
</header>
|
||||||
|
<ul>
|
||||||
|
<li class="active"><span>item</span></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* === ~= word matching === */
|
||||||
|
|
||||||
|
/* Should match: "foo" is a whole word in class="foo bar" */
|
||||||
|
.foo { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "bar" is a whole word in class="foo bar" */
|
||||||
|
.bar { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "foobar" is the whole class value */
|
||||||
|
.foobar { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "bar-foo" is a whole word (hyphen not whitespace) */
|
||||||
|
.bar-foo { color: green; }
|
||||||
|
|
||||||
|
/* Should match: "baz" is a whole word in class="bar-foo baz" */
|
||||||
|
.baz { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: "foob" is not a word in any element's class */
|
||||||
|
.foob { color: red; }
|
||||||
|
|
||||||
|
/* Should NOT match: "afoo" is a word but "foo-x" is not "foo" */
|
||||||
|
[class~="foo-x"] { color: green; }
|
||||||
|
|
||||||
|
/* Attribute selector with ~= operator directly */
|
||||||
|
[class~="afoo"] { color: green; }
|
||||||
|
|
||||||
|
/* === Deep combinator chains (4+ levels) === */
|
||||||
|
|
||||||
|
/* Should match: exact chain main > article > section > div > span */
|
||||||
|
main > article > section > div > span { color: green; }
|
||||||
|
|
||||||
|
/* Should match: descendant chain */
|
||||||
|
main article section div span { color: green; }
|
||||||
|
|
||||||
|
/* Should match: mixed combinators */
|
||||||
|
main > article section > div span { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: wrong nesting order */
|
||||||
|
main > article > div > section > span { color: red; }
|
||||||
|
|
||||||
|
/* === :has() combined with other selectors === */
|
||||||
|
|
||||||
|
/* Should match: nav.primary has <a> descendant */
|
||||||
|
nav:has(a).primary { color: green; }
|
||||||
|
|
||||||
|
/* Should match: nav.secondary has <button> descendant */
|
||||||
|
nav:has(button).secondary { color: green; }
|
||||||
|
|
||||||
|
/* Should NOT match: nav.primary doesn't have <button> */
|
||||||
|
nav:has(button).primary { color: red; }
|
||||||
|
|
||||||
|
/* Multiple :has() on same element */
|
||||||
|
main:has(article):has(span) { color: green; }
|
||||||
|
|
||||||
|
/* :has() with child combinator */
|
||||||
|
main:has(> article) { color: green; }
|
||||||
|
|
||||||
|
/* === Escaped selectors === */
|
||||||
|
.a\-b { color: green; }
|
||||||
|
|
||||||
|
/* === :is()/:where()/:not() with deep selectors === */
|
||||||
|
|
||||||
|
/* :is() with matching selector */
|
||||||
|
header :is(h1) { color: green; }
|
||||||
|
|
||||||
|
/* :where() with matching selector */
|
||||||
|
ul :where(li) { color: green; }
|
||||||
|
|
||||||
|
/* :not() — should match span since it's not a div */
|
||||||
|
span:not(div) { color: green; }
|
||||||
|
|
||||||
|
/* :is() with deep combinator */
|
||||||
|
ul :is(li > span) { color: green; }
|
||||||
|
|
||||||
|
/* :not() with class — p.a-b is :not(.unused) */
|
||||||
|
p:not(.unused) { color: green; }
|
||||||
|
|
||||||
|
/* Complex: :has() + :is() */
|
||||||
|
ul:has(li) :is(span) { color: green; }
|
||||||
|
</style>
|
||||||
Loading…
Reference in new issue