@pyreon/styler 0.24.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -60,6 +60,27 @@ type Interpolation<P extends object = Record<string, unknown>> = string | number
60
60
  declare class CSSResult {
61
61
  readonly strings: TemplateStringsArray;
62
62
  readonly values: Interpolation[];
63
+ /**
64
+ * Memoized result of `isDynamic(this)`. Populated on first access from
65
+ * `shared.ts#isDynamic`. CSSResult instances are typically created once
66
+ * at module load (one per `css\`...\`` literal) and reused everywhere —
67
+ * a `styled()` component, a `useCSS` consumer, a nested interpolation,
68
+ * etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
69
+ * Ported from vitus-labs `c483cabc`.
70
+ */
71
+ _isDynamic: boolean | undefined;
72
+ /**
73
+ * Memoized resolved CSS string for STATIC CSSResults — populated by
74
+ * `resolveValue` the first time a known-static nested CSSResult is
75
+ * resolved. Safe because props don't affect output when there are no
76
+ * function interpolations. Skipped for dynamic CSSResults (the resolved
77
+ * string depends on props each call). Common pattern: a shared static
78
+ * snippet interpolated into many dynamic components — pre-cache, that
79
+ * snippet was re-resolved per-render of every consumer. Ported from
80
+ * vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
81
+ * ~2.5× speedup on the 8-repeated-resolve micro).
82
+ */
83
+ _staticResolved: string | undefined;
63
84
  constructor(strings: TemplateStringsArray, values: Interpolation[]);
64
85
  /** Resolve with empty props — useful for static templates, testing, and debugging. */
65
86
  toString(): string;
@@ -121,8 +142,7 @@ declare const createGlobalStyle: (strings: TemplateStringsArray, ...values: Inte
121
142
  */
122
143
  /** FNV-1a offset basis — starting state for streaming hash. */
123
144
  declare const HASH_INIT = 2166136261;
124
- /**
125
- * Feed a string segment into the running hash state.
145
+ /** Feed a string segment into the running hash state.
126
146
  * Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
127
147
  */
128
148
  declare const hashUpdate: (init: number, str: string) => number;
package/lib/index.js CHANGED
@@ -11,6 +11,27 @@ const _countSink$2 = globalThis;
11
11
  var CSSResult = class {
12
12
  strings;
13
13
  values;
14
+ /**
15
+ * Memoized result of `isDynamic(this)`. Populated on first access from
16
+ * `shared.ts#isDynamic`. CSSResult instances are typically created once
17
+ * at module load (one per `css\`...\`` literal) and reused everywhere —
18
+ * a `styled()` component, a `useCSS` consumer, a nested interpolation,
19
+ * etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
20
+ * Ported from vitus-labs `c483cabc`.
21
+ */
22
+ _isDynamic = void 0;
23
+ /**
24
+ * Memoized resolved CSS string for STATIC CSSResults — populated by
25
+ * `resolveValue` the first time a known-static nested CSSResult is
26
+ * resolved. Safe because props don't affect output when there are no
27
+ * function interpolations. Skipped for dynamic CSSResults (the resolved
28
+ * string depends on props each call). Common pattern: a shared static
29
+ * snippet interpolated into many dynamic components — pre-cache, that
30
+ * snippet was re-resolved per-render of every consumer. Ported from
31
+ * vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
32
+ * ~2.5× speedup on the 8-repeated-resolve micro).
33
+ */
34
+ _staticResolved = void 0;
14
35
  constructor(strings, values) {
15
36
  this.strings = strings;
16
37
  this.values = values;
@@ -100,7 +121,13 @@ const normalizeCSS = (css) => {
100
121
  const resolveValue = (value, props) => {
101
122
  if (value == null || value === false || value === true) return "";
102
123
  if (typeof value === "function") return resolveValue(value(props), props);
103
- if (value instanceof CSSResult) return resolve(value.strings, value.values, props);
124
+ if (value instanceof CSSResult) {
125
+ if (value._isDynamic === false) {
126
+ if (value._staticResolved === void 0) value._staticResolved = resolve(value.strings, value.values, {});
127
+ return value._staticResolved;
128
+ }
129
+ return resolve(value.strings, value.values, props);
130
+ }
104
131
  if (Array.isArray(value)) {
105
132
  let arrayResult = "";
106
133
  for (let i = 0; i < value.length; i++) arrayResult += resolveValue(value[i], props);
@@ -129,7 +156,7 @@ const css = (strings, ...values) => new CSSResult(strings, values);
129
156
  * elements (which causes warnings). Props starting with `$` are
130
157
  * transient (styling-only) and are always filtered out.
131
158
  */
132
- const HTML_PROPS = new Set([
159
+ const HTML_PROPS_LIST = [
133
160
  "className",
134
161
  "class",
135
162
  "dangerouslySetInnerHTML",
@@ -311,7 +338,9 @@ const HTML_PROPS = new Set([
311
338
  "value",
312
339
  "width",
313
340
  "wrap"
314
- ]);
341
+ ];
342
+ const HTML_PROPS = Object.create(null);
343
+ for (const k of HTML_PROPS_LIST) HTML_PROPS[k] = true;
315
344
  /**
316
345
  * Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
317
346
  * Rejects unknown props and $-prefixed transient props.
@@ -325,7 +354,7 @@ const filterProps = (props) => {
325
354
  filtered[key] = props[key];
326
355
  continue;
327
356
  }
328
- if (HTML_PROPS.has(key)) filtered[key] = props[key];
357
+ if (key in HTML_PROPS) filtered[key] = props[key];
329
358
  }
330
359
  return filtered;
331
360
  };
@@ -373,7 +402,7 @@ const buildProps = (rawProps, generatedCls, isDOM, customFilter) => {
373
402
  copyDescriptor(key);
374
403
  continue;
375
404
  }
376
- if (HTML_PROPS.has(key)) copyDescriptor(key);
405
+ if (key in HTML_PROPS) copyDescriptor(key);
377
406
  }
378
407
  return result;
379
408
  };
@@ -387,7 +416,13 @@ const buildProps = (rawProps, generatedCls, isDOM, customFilter) => {
387
416
  const isDynamic = (v) => {
388
417
  if (typeof v === "function") return true;
389
418
  if (Array.isArray(v)) return v.some(isDynamic);
390
- if (v instanceof CSSResult) return v.values.some(isDynamic);
419
+ if (v instanceof CSSResult) {
420
+ const cached = v._isDynamic;
421
+ if (cached !== void 0) return cached;
422
+ const r = v.values.some(isDynamic);
423
+ v._isDynamic = r;
424
+ return r;
425
+ }
391
426
  return false;
392
427
  };
393
428
 
@@ -402,16 +437,12 @@ const isDynamic = (v) => {
402
437
  /** FNV-1a offset basis — starting state for streaming hash. */
403
438
  const HASH_INIT = 2166136261;
404
439
  const FNV_PRIME = 16777619;
405
- /**
406
- * Feed a string segment into the running hash state.
440
+ /** Feed a string segment into the running hash state.
407
441
  * Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
408
442
  */
409
443
  const hashUpdate = (init, str) => {
410
444
  let h = init;
411
- for (let i = 0; i < str.length; i++) {
412
- h ^= str.charCodeAt(i);
413
- h = Math.imul(h, FNV_PRIME);
414
- }
445
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), FNV_PRIME);
415
446
  return h;
416
447
  };
417
448
  /** Finalize a hash state into a base-36 class name suffix. */
@@ -565,13 +596,14 @@ var StyleSheet = class {
565
596
  };
566
597
  const atRules = [];
567
598
  const baseParts = [];
599
+ const len = cssText.length;
568
600
  let depth = 0;
569
601
  let atStart = -1;
570
602
  let lastBase = 0;
571
- for (let i = 0; i < cssText.length; i++) {
572
- const ch = cssText[i];
573
- if (ch === "{") depth++;
574
- else if (ch === "}") {
603
+ for (let i = 0; i < len; i++) {
604
+ const ch = cssText.charCodeAt(i);
605
+ if (ch === 123) depth++;
606
+ else if (ch === 125) {
575
607
  depth--;
576
608
  if (depth === 0 && atStart >= 0) {
577
609
  const openBrace = cssText.indexOf("{", atStart);
@@ -581,7 +613,7 @@ var StyleSheet = class {
581
613
  atStart = -1;
582
614
  lastBase = i + 1;
583
615
  }
584
- } else if (depth === 0 && ch === "@" && atStart < 0) {
616
+ } else if (depth === 0 && ch === 64 && atStart < 0) {
585
617
  const remaining = cssText.slice(i, i + 20);
586
618
  if (/^@(?:media|supports|container)\b/.test(remaining)) {
587
619
  const baseBefore = cssText.slice(lastBase, i).trim();
@@ -676,12 +708,13 @@ var StyleSheet = class {
676
708
  */
677
709
  splitRules(cssText) {
678
710
  const rules = [];
711
+ const len = cssText.length;
679
712
  let depth = 0;
680
713
  let start = 0;
681
- for (let i = 0; i < cssText.length; i++) {
682
- const ch = cssText[i];
683
- if (ch === "{") depth++;
684
- else if (ch === "}") {
714
+ for (let i = 0; i < len; i++) {
715
+ const ch = cssText.charCodeAt(i);
716
+ if (ch === 123) depth++;
717
+ else if (ch === 125) {
685
718
  depth--;
686
719
  if (depth === 0) {
687
720
  const rule = cssText.slice(start, i + 1).trim();
@@ -897,7 +930,7 @@ nativeCompat(ThemeProvider);
897
930
  const createGlobalStyle = (strings, ...values) => {
898
931
  if (!values.some(isDynamic)) {
899
932
  const cssText = normalizeCSS(resolve(strings, values, {}));
900
- if (cssText.trim()) sheet.insertGlobal(cssText);
933
+ if (cssText.length > 0) sheet.insertGlobal(cssText);
901
934
  const StaticGlobal = () => null;
902
935
  return StaticGlobal;
903
936
  }
@@ -907,7 +940,7 @@ const createGlobalStyle = (strings, ...values) => {
907
940
  ...props,
908
941
  theme
909
942
  }));
910
- if (cssText.trim()) sheet.insertGlobal(cssText);
943
+ if (cssText.length > 0) sheet.insertGlobal(cssText);
911
944
  return null;
912
945
  };
913
946
  return DynamicGlobal;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/styler",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Lightweight CSS-in-JS engine for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@pyreon/manifest": "0.13.1",
46
46
  "@pyreon/test-utils": "^0.13.11",
47
- "@pyreon/typescript": "^0.24.0",
47
+ "@pyreon/typescript": "^0.24.2",
48
48
  "@vitest/browser-playwright": "^4.1.4",
49
49
  "@vitus-labs/tools-rolldown": "^2.4.0"
50
50
  },
@@ -52,7 +52,7 @@
52
52
  "node": ">= 22"
53
53
  },
54
54
  "dependencies": {
55
- "@pyreon/core": "^0.24.0",
56
- "@pyreon/reactivity": "^0.24.0"
55
+ "@pyreon/core": "^0.24.2",
56
+ "@pyreon/reactivity": "^0.24.2"
57
57
  }
58
58
  }
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { css } from "../css"
3
3
  import { CSSResult, normalizeCSS, resolve, resolveValue } from "../resolve"
4
+ import { isDynamic } from "../shared"
4
5
 
5
6
  // Helper to create a TemplateStringsArray
6
7
  const tsa = (strings: readonly string[]): TemplateStringsArray => {
@@ -247,3 +248,61 @@ describe("normalizeCSS", () => {
247
248
  })
248
249
  })
249
250
  })
251
+
252
+ // Behavioural lock-in for the CSSResult._staticResolved cache (ported from
253
+ // vitus-labs `754cd203` + lock-in `60fc25c1`). Common pattern: a shared
254
+ // static snippet interpolated into many dynamic components. Without this
255
+ // cache the snippet's resolve work was paid once per dynamic render of
256
+ // every consumer; with it, the resolve fires once per snippet, total.
257
+ describe("CSSResult._staticResolved cache", () => {
258
+ it("populates _staticResolved on first resolveValue of a known-static CSSResult", () => {
259
+ const inner = css`color: red;`
260
+ // Pre-classify as static via isDynamic (the same call shared.ts makes
261
+ // at styled-component creation time).
262
+ isDynamic(inner)
263
+ expect(inner._isDynamic).toBe(false)
264
+ expect(inner._staticResolved).toBe(undefined)
265
+
266
+ resolveValue(inner, {})
267
+ expect(inner._staticResolved).toBe("color: red;")
268
+ })
269
+
270
+ it("returns cached _staticResolved on subsequent resolveValue calls", () => {
271
+ const inner = css`padding: 12px;`
272
+ isDynamic(inner)
273
+ resolveValue(inner, {})
274
+ expect(inner._staticResolved).toBe("padding: 12px;")
275
+
276
+ // Mutate values to a sentinel that would change the resolved output if
277
+ // recomputed. The cache MUST return the prior result.
278
+ ;(inner as unknown as { values: unknown[] }).values = ["SENTINEL"]
279
+ expect(resolveValue(inner, {})).toBe("padding: 12px;")
280
+ })
281
+
282
+ it("does NOT cache dynamic CSSResults (props vary per call)", () => {
283
+ const dyn = css`color: ${(p: Record<string, unknown>) => p.c as string};`
284
+ isDynamic(dyn)
285
+ expect(dyn._isDynamic).toBe(true)
286
+
287
+ // Resolve twice with different props; cache should not be populated.
288
+ resolveValue(dyn, { c: "red" })
289
+ expect(dyn._staticResolved).toBe(undefined)
290
+ resolveValue(dyn, { c: "blue" })
291
+ expect(dyn._staticResolved).toBe(undefined)
292
+ })
293
+
294
+ it("skips cache when _isDynamic is undefined (not yet classified)", () => {
295
+ // Construct a CSSResult directly without going through isDynamic.
296
+ // resolveValue's cache check is `_isDynamic === false` (strict), so an
297
+ // unclassified CSSResult falls through to the regular resolve path
298
+ // — the regular path takes the SECOND branch (`return resolve(...)`)
299
+ // and does NOT populate the cache.
300
+ const tpl = Object.assign(["color: ", ";"], {
301
+ raw: ["color: ", ";"],
302
+ }) as unknown as TemplateStringsArray
303
+ const r = new CSSResult(tpl, ["red"])
304
+ expect(r._isDynamic).toBe(undefined)
305
+ expect(resolveValue(r, {})).toBe("color: red;")
306
+ expect(r._staticResolved).toBe(undefined) // cache stays unpopulated
307
+ })
308
+ })
@@ -88,4 +88,46 @@ describe('isDynamic', () => {
88
88
  )
89
89
  expect(isDynamic(result)).toBe(true)
90
90
  })
91
+
92
+ // Behavioural lock-in for the CSSResult._isDynamic memoization (ported
93
+ // from vitus-labs `c483cabc` + lock-in `60fc25c1`). Without these tests
94
+ // a future regression that removed the cache would only show up as a
95
+ // perf regression, never as a test failure.
96
+ describe('CSSResult _isDynamic memoization', () => {
97
+ it('populates _isDynamic on first call for dynamic templates', () => {
98
+ const r = css`color: ${() => 'red'};`
99
+ expect(r._isDynamic).toBe(undefined)
100
+ isDynamic(r)
101
+ expect(r._isDynamic).toBe(true)
102
+ })
103
+
104
+ it('populates _isDynamic on first call for static templates', () => {
105
+ const r = css`color: ${'red'};`
106
+ expect(r._isDynamic).toBe(undefined)
107
+ isDynamic(r)
108
+ expect(r._isDynamic).toBe(false)
109
+ })
110
+
111
+ it('returns cached result on subsequent calls without rescanning values', () => {
112
+ const r = css`color: ${() => 'red'};`
113
+ const first = isDynamic(r)
114
+ expect(first).toBe(true)
115
+ expect(r._isDynamic).toBe(true)
116
+
117
+ // Mutate values to a sentinel that would invert the answer if rescanned.
118
+ // The memoized path must NOT consult `values` again — it should return
119
+ // the cached `_isDynamic` directly.
120
+ ;(r as unknown as { values: unknown[] }).values = ['static-only']
121
+ expect(isDynamic(r)).toBe(true) // still uses cached value, not rescan
122
+ })
123
+
124
+ it('memoizes nested CSSResults independently', () => {
125
+ const inner = css`color: ${() => 'red'};`
126
+ const outer = css`${inner}`
127
+ isDynamic(outer)
128
+ // Recursing through outer populates inner too.
129
+ expect(inner._isDynamic).toBe(true)
130
+ expect(outer._isDynamic).toBe(true)
131
+ })
132
+ })
91
133
  })
package/src/forward.ts CHANGED
@@ -4,8 +4,15 @@
4
4
  * transient (styling-only) and are always filtered out.
5
5
  */
6
6
 
7
- // Common HTML attributes, event handlers, and ARIA/data attributes
8
- const HTML_PROPS = new Set([
7
+ // Common HTML attributes, event handlers, and ARIA/data attributes.
8
+ //
9
+ // Using a plain object with `key in HTML_PROPS` instead of `Set.has(key)`:
10
+ // V8 inlines `in` checks via hidden-class lookups (the object has a fixed
11
+ // shape at module load and never changes), which is meaningfully faster
12
+ // than going through the Set protocol on hot prop-filter paths. Measured
13
+ // upstream (vitus-labs `be471b19`): +19% on the 5-lookup mix benchmark
14
+ // (4 hits + 1 miss).
15
+ const HTML_PROPS_LIST = [
9
16
  // Core props
10
17
  'className',
11
18
  'class',
@@ -190,7 +197,13 @@ const HTML_PROPS = new Set([
190
197
  'value',
191
198
  'width',
192
199
  'wrap',
193
- ])
200
+ ] as const
201
+
202
+ // Build the lookup object once at module load. `null`-prototype keeps the
203
+ // object's hidden class lean and means `in` checks don't accidentally pick
204
+ // up `Object.prototype` keys.
205
+ const HTML_PROPS: Record<string, true> = Object.create(null)
206
+ for (const k of HTML_PROPS_LIST) HTML_PROPS[k] = true
194
207
 
195
208
  /**
196
209
  * Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
@@ -212,8 +225,9 @@ export const filterProps = (props: Record<string, unknown>): Record<string, unkn
212
225
  continue
213
226
  }
214
227
 
215
- // Keep known HTML props
216
- if (HTML_PROPS.has(key)) {
228
+ // Keep known HTML props — `in` against the null-prototype lookup
229
+ // object beats `Set.has` on the hot DOM-filter path.
230
+ if (key in HTML_PROPS) {
217
231
  filtered[key] = props[key]
218
232
  }
219
233
  }
@@ -288,7 +302,7 @@ export const buildProps = (
288
302
  copyDescriptor(key)
289
303
  continue
290
304
  }
291
- if (HTML_PROPS.has(key)) copyDescriptor(key)
305
+ if (key in HTML_PROPS) copyDescriptor(key)
292
306
  }
293
307
  return result
294
308
  }
@@ -26,8 +26,11 @@ export const createGlobalStyle = (
26
26
  if (!hasDynamicValues) {
27
27
  const cssText = normalizeCSS(resolve(strings, values, {}))
28
28
 
29
- // Inject into sheet immediately
30
- if (cssText.trim()) sheet.insertGlobal(cssText)
29
+ // Inject into sheet immediately. `normalizeCSS` already strips
30
+ // leading/trailing whitespace, so a length check is equivalent to the
31
+ // prior `.trim()` (no O(n) whitespace scan, no string allocation).
32
+ // Ported from vitus-labs `be471b19`.
33
+ if (cssText.length > 0) sheet.insertGlobal(cssText)
31
34
 
32
35
  const StaticGlobal: ComponentFn = () => null
33
36
  return StaticGlobal
@@ -39,7 +42,9 @@ export const createGlobalStyle = (
39
42
  const allProps = { ...props, theme }
40
43
  const cssText = normalizeCSS(resolve(strings, values, allProps))
41
44
 
42
- if (cssText.trim()) sheet.insertGlobal(cssText)
45
+ // Length check — `normalizeCSS` already trims. Ported from
46
+ // vitus-labs `be471b19`.
47
+ if (cssText.length > 0) sheet.insertGlobal(cssText)
43
48
 
44
49
  return null
45
50
  }
package/src/hash.ts CHANGED
@@ -10,15 +10,13 @@ export const HASH_INIT = 2166136261
10
10
 
11
11
  const FNV_PRIME = 16777619
12
12
 
13
- /**
14
- * Feed a string segment into the running hash state.
13
+ /** Feed a string segment into the running hash state.
15
14
  * Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
16
15
  */
17
16
  export const hashUpdate = (init: number, str: string): number => {
18
17
  let h = init
19
18
  for (let i = 0; i < str.length; i++) {
20
- h ^= str.charCodeAt(i)
21
- h = Math.imul(h, FNV_PRIME)
19
+ h = Math.imul(h ^ str.charCodeAt(i), FNV_PRIME)
22
20
  }
23
21
  return h
24
22
  }
package/src/resolve.ts CHANGED
@@ -40,6 +40,29 @@ export type Interpolation<P extends object = Record<string, unknown>> =
40
40
  * deferred until a styled component renders (or until explicitly resolved).
41
41
  */
42
42
  export class CSSResult {
43
+ /**
44
+ * Memoized result of `isDynamic(this)`. Populated on first access from
45
+ * `shared.ts#isDynamic`. CSSResult instances are typically created once
46
+ * at module load (one per `css\`...\`` literal) and reused everywhere —
47
+ * a `styled()` component, a `useCSS` consumer, a nested interpolation,
48
+ * etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
49
+ * Ported from vitus-labs `c483cabc`.
50
+ */
51
+ _isDynamic: boolean | undefined = undefined
52
+
53
+ /**
54
+ * Memoized resolved CSS string for STATIC CSSResults — populated by
55
+ * `resolveValue` the first time a known-static nested CSSResult is
56
+ * resolved. Safe because props don't affect output when there are no
57
+ * function interpolations. Skipped for dynamic CSSResults (the resolved
58
+ * string depends on props each call). Common pattern: a shared static
59
+ * snippet interpolated into many dynamic components — pre-cache, that
60
+ * snippet was re-resolved per-render of every consumer. Ported from
61
+ * vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
62
+ * ~2.5× speedup on the 8-repeated-resolve micro).
63
+ */
64
+ _staticResolved: string | undefined = undefined
65
+
43
66
  constructor(
44
67
  readonly strings: TemplateStringsArray,
45
68
  readonly values: Interpolation[],
@@ -173,8 +196,21 @@ export const resolveValue = (value: Interpolation, props: Record<string, any>):
173
196
  // function interpolation → call with props/theme context, resolve result
174
197
  if (typeof value === 'function') return resolveValue(value(props) as Interpolation, props)
175
198
 
176
- // nested CSSResult → recursively resolve
177
- if (value instanceof CSSResult) return resolve(value.strings, value.values, props)
199
+ // nested CSSResult → recursively resolve, with static-result memoization.
200
+ // When `_isDynamic === false` (populated by shared.ts#isDynamic at styled
201
+ // component creation), the resolved string is independent of props and can
202
+ // be cached on the instance. Saves re-walking strings/values for every
203
+ // consumer of a shared static snippet. Ported from vitus-labs `754cd203`;
204
+ // measured upstream: ~2.5× speedup on 8-repeated-resolve micro.
205
+ if (value instanceof CSSResult) {
206
+ if (value._isDynamic === false) {
207
+ if (value._staticResolved === undefined) {
208
+ value._staticResolved = resolve(value.strings, value.values, {})
209
+ }
210
+ return value._staticResolved
211
+ }
212
+ return resolve(value.strings, value.values, props)
213
+ }
178
214
 
179
215
  // array of results (e.g. from makeItResponsive's breakpoints.map())
180
216
  if (Array.isArray(value)) {
package/src/shared.ts CHANGED
@@ -7,6 +7,16 @@ import { CSSResult, type Interpolation } from './resolve'
7
7
  export const isDynamic = (v: Interpolation): boolean => {
8
8
  if (typeof v === 'function') return true
9
9
  if (Array.isArray(v)) return v.some(isDynamic)
10
- if (v instanceof CSSResult) return v.values.some(isDynamic)
10
+ if (v instanceof CSSResult) {
11
+ // Memoize per-instance — CSSResults are created once at module level
12
+ // (one per `css\`...\`` literal) and reused across many `styled()` /
13
+ // `useCSS()` / nested-interpolation checks. Avoids rescanning whole
14
+ // sub-trees on every consumer. Ported from vitus-labs `c483cabc`.
15
+ const cached = v._isDynamic
16
+ if (cached !== undefined) return cached
17
+ const r = v.values.some(isDynamic)
18
+ v._isDynamic = r
19
+ return r
20
+ }
11
21
  return false
12
22
  }
package/src/sheet.ts CHANGED
@@ -218,16 +218,21 @@ export class StyleSheet {
218
218
 
219
219
  const atRules: string[] = []
220
220
  const baseParts: string[] = []
221
+ const len = cssText.length
221
222
  let depth = 0
222
223
  let atStart = -1
223
224
  let lastBase = 0
224
225
 
225
- for (let i = 0; i < cssText.length; i++) {
226
- const ch = cssText[i]
226
+ // `charCodeAt(i)` returns a primitive int; `cssText[i]` allocates a
227
+ // fresh 1-char string in V8 per iteration. On long stylesheets with
228
+ // at-rule blocks the per-char allocation dominates. Ported from
229
+ // vitus-labs `c483cabc`.
230
+ for (let i = 0; i < len; i++) {
231
+ const ch = cssText.charCodeAt(i)
227
232
 
228
- if (ch === '{') {
233
+ if (ch === 123 /* { */) {
229
234
  depth++
230
- } else if (ch === '}') {
235
+ } else if (ch === 125 /* } */) {
231
236
  depth--
232
237
  if (depth === 0 && atStart >= 0) {
233
238
  // End of a tracked at-rule block — extract and wrap with selector
@@ -240,7 +245,7 @@ export class StyleSheet {
240
245
  atStart = -1
241
246
  lastBase = i + 1
242
247
  }
243
- } else if (depth === 0 && ch === '@' && atStart < 0) {
248
+ } else if (depth === 0 && ch === 64 /* @ */ && atStart < 0) {
244
249
  // Check if this starts a splittable at-rule (not @keyframes, @font-face, etc.)
245
250
  const remaining = cssText.slice(i, i + 20)
246
251
  if (/^@(?:media|supports|container)\b/.test(remaining)) {
@@ -377,13 +382,16 @@ export class StyleSheet {
377
382
  */
378
383
  private splitRules(cssText: string): string[] {
379
384
  const rules: string[] = []
385
+ const len = cssText.length
380
386
  let depth = 0
381
387
  let start = 0
382
388
 
383
- for (let i = 0; i < cssText.length; i++) {
384
- const ch = cssText[i]
385
- if (ch === '{') depth++
386
- else if (ch === '}') {
389
+ // `charCodeAt(i)` returns a primitive int; `cssText[i]` allocates a
390
+ // fresh 1-char string per iteration. Ported from vitus-labs `c483cabc`.
391
+ for (let i = 0; i < len; i++) {
392
+ const ch = cssText.charCodeAt(i)
393
+ if (ch === 123 /* { */) depth++
394
+ else if (ch === 125 /* } */) {
387
395
  depth--
388
396
  if (depth === 0) {
389
397
  const rule = cssText.slice(start, i + 1).trim()