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