Commit 98be4f1
perf(components): scan for component tags without copying the document per tag (#1945)
`findComponentTags` walks the document looking at every `<`. To test whether a
tag name matched, it sliced the whole remaining document and ran an anchored
`^...` pattern against the copy:
const afterLt = html.slice(tagStart + 1)
const m = afterLt.match(new RegExp(`^(${tagPattern.source})(?![\w.:-])`))
Both halves are per-`<`: a copy of the rest of the page, and a regex compile.
It runs three times per pass (kebab, PascalCase, lowercase) and again for every
nested component.
A sticky matcher does the same test at an offset in the original string. Same
pattern, same lookahead, no copy, compiled once per source rather than once per
character.
Measured with a String.prototype allocation profiler over one steady-state
render, on four fixture shapes:
before after
component-dense 107.57MB 14.28MB -87%
expression-dense 18.88MB 3.83MB -80%
stores + composables 6.02MB 3.91MB -35%
plain signals page 3.53MB 3.53MB 0% (no component tags)
On the component-dense page this one site was 95.5MB of the 107.6MB the render
allocated -- 89% of it, 4,250 slices averaging 23KB each. It is the largest
single item behind this issue by a wide margin; the three commits before this
one moved 377KB between them.
The plain-signals row is not a disappointment, it is the control: a page with no
component tags never entered the loop, and its number does not move.
Three things the rewrite had to preserve, each pinned by a sabotage run:
The #1845 lookahead. `(?![\w.:-])` is what stops the pattern matching a PREFIX
of a longer tag -- `<ion-button />` matched `ion`, lost the hyphen that marks it
a custom element, and resolved `ion.stx` from disk, splicing an ENOENT into the
page. Dropping it from the shared matcher fails 3 component tests.
The offset. A sticky regex matches at `lastIndex`, so it must be set to
`tagStart + 1` before every exec. Leaving it at 0 fails 78 tests.
Flags. The old matcher was built from `.source` alone, so it was case-sensitive
whatever the caller's pattern said, and the kebab/Pascal/lowercase dispatch
depends on that. The sticky matcher adds `y` and nothing else.
The new matchers are cached by pattern source. The three call sites declare
their patterns as literals inside a function, so each call passes a fresh RegExp
object with a familiar source -- keying on the object would miss every time, and
keying on the source holds three entries. A caller building patterns dynamically
would need to bound it; none does.
Verified byte-identical against HEAD across 12 renders (4 fixture shapes x SEO
on / SEO off / colorMode). 5 new tests covering the sticky-index risks the
existing suite does not reach. Suite 12,709 pass / 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>1 parent 132ef77 commit 98be4f1
2 files changed
Lines changed: 111 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
94 | 132 | | |
95 | 133 | | |
96 | 134 | | |
| |||
110 | 148 | | |
111 | 149 | | |
112 | 150 | | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
122 | 155 | | |
123 | 156 | | |
124 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
0 commit comments