From 0ac74411d946fa445dedeac332553b027d5757fb Mon Sep 17 00:00:00 2001 From: Jaydeep Agravat Date: Thu, 5 Nov 2026 11:29:09 +0530 Subject: [PATCH 1/5] =?UTF-8?q?perf:=20cache=20native=20styles=20that=20us?= =?UTF-8?q?e=20data-[=E2=80=A6]=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A class string with a data-[…] variant was never cached on native, so it was resolved again on every render. Style records now carry the names of the data-* attributes they read; the store collects them once per class string and adds those props' values to the cache key, normalized the way the generated match functions compare them. Closes #733 --- CONTEXT.md | 6 +- .../css-processor/addMetaToStylesTemplate.ts | 2 +- packages/uniwind/src/core/native/store.ts | 78 ++++++++++++++----- packages/uniwind/src/core/types.ts | 2 +- .../native/core/data-attributes-cache.test.ts | 72 +++++++++++++++++ .../native/styles-parsing/matches.test.ts | 4 +- 6 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 packages/uniwind/tests/native/core/data-attributes-cache.test.ts diff --git a/CONTEXT.md b/CONTEXT.md index e431f789..7ca14b8a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -70,11 +70,11 @@ Native runtime: - Build output injects a generated stylesheet callback into `Uniwind.__reinit(...)`. - `UniwindStore` holds generated style records, theme variables, scoped variables, runtime state, and per-theme caches. - `UniwindStore.getStyles(className, props, state, context)` resolves classes into React Native style objects. -- Cache keys include class names, component state, whether theme is scoped, layout direction, and a key derived from the merged `ScopedVariables` map. +- Cache keys include class names, component state, whether theme is scoped, layout direction, a key derived from the merged `ScopedVariables` map, and the values of the `data-*` props the class string's rules use. Those attribute names are collected once per class string and reset on reinit; values are normalized the way match predicates compare them (`true`/`"true"` and `false`/`"false"` are equal, other strings match only themselves, other values never match). - During resolve, `ScopedVariables` overrides are overlaid onto a prototype-chained clone of the theme vars so unset variables fall through to the theme. - Resolved styles subscribe to only dependencies they use, then invalidate cache entries on change. - Runtime dependencies are represented by `StyleDependency`: theme, dimensions, orientation, insets, font scale, RTL, adaptive themes, and variables. -- Native style resolution calls each rule's build-generated `matches(rt, context, props, state, vars)` predicate. Predicates contain only that rule's screen width/height, orientation, theme, RTL, active/focus/disabled, and `data-*` conditions, respecting scoped themes and layout direction. Data-dependent rules carry a boolean cache-bypass flag; condition metadata stays at build time. +- Native style resolution calls each rule's build-generated `matches(rt, context, props, state, vars)` predicate. Predicates contain only that rule's screen width/height, orientation, theme, RTL, active/focus/disabled, and `data-*` conditions, respecting scoped themes and layout direction. Data-dependent rules carry the names of the `data-*` attributes they read so cache keys can include those props; expected values and other condition metadata stay at build time. - Native post-processing adapts CSS concepts to RN shapes, including line-height multipliers, shadows, transforms, gradients, visibility, borders, outlines, font variants, and filters. Web runtime: @@ -147,7 +147,7 @@ Build-time stylesheet templates collect each rule as `{ style, meta }`: CSS decl Important concepts: -- A runtime `Style` record stores entries, a generated `matches` function, a `hasDataAttributes` flag, dependencies, per-class index, class name, important properties, and normal and important cascade ranks (`complexity` and `importantComplexity`). Media/selector condition metadata stays in build-time templates. Platform conditions are filtered at build time and are not emitted as a runtime flag. +- A runtime `Style` record stores entries, a generated `matches` function, the `data-*` attribute names it reads (or `null`), dependencies, per-class index, class name, important properties, and normal and important cascade ranks (`complexity` and `importantComplexity`). Media/selector condition metadata stays in build-time templates. Platform conditions are filtered at build time and are not emitted as a runtime flag. - Native property precedence is computed at build time from importance, cascade-layer priority, supported selector specificity, and compiled CSS order. Named layer statements, reopened layers, nested layers, anonymous layers, and unlayered rules participate; important declarations reverse layer priority. Every important rank is above every normal rank. Runtime resolution filters matching rules and compares the appropriate rank per property, independent of className token order or minimum-width priority. - Specificity covers the selectors native already supports: classes, state pseudo-classes and data attributes contribute; `:where(...)` and media queries do not. Nested selectors inherit their parent's specificity. This is author-rule precedence for supported selectors, not a general DOM selector engine or support for inheritance, animations, transitions, `@scope`, or `revert-layer`. - CSS variables live in `vars`; theme and platform-scoped variables live in `scopedVars` with internal prefixes. diff --git a/packages/uniwind/src/bundler/css-processor/addMetaToStylesTemplate.ts b/packages/uniwind/src/bundler/css-processor/addMetaToStylesTemplate.ts index 3e5a7cd5..f7dc5d59 100644 --- a/packages/uniwind/src/bundler/css-processor/addMetaToStylesTemplate.ts +++ b/packages/uniwind/src/bundler/css-processor/addMetaToStylesTemplate.ts @@ -128,7 +128,7 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder) => { return { entries, matches, - hasDataAttributes: dataAttributes !== null, + dataAttributeNames: dataAttributes !== null ? Object.keys(dataAttributes).map(makeSafeForSerialization) : null, dependencies: dependencies.size > 0 ? Array.from(dependencies) : null, index, className: makeSafeForSerialization(className), diff --git a/packages/uniwind/src/core/native/store.ts b/packages/uniwind/src/core/native/store.ts index 65c2314d..efcf3219 100644 --- a/packages/uniwind/src/core/native/store.ts +++ b/packages/uniwind/src/core/native/store.ts @@ -21,11 +21,34 @@ type StylesResult = { const emptyState: StylesResult = { styles: {}, dependencies: [], dependencySum: 0 } +const splitClassNames = (classNames: string) => + classNames.includes('\n') + ? classNames.split(/\s+/).filter(Boolean) + : classNames.split(' ') + +// Reduces a data attribute value to what the generated match functions can tell apart: +// true and "true" match the same rules, so do false and "false", other strings only match +// themselves, and any other value never matches. +const toDataAttributeCacheValue = (value: unknown) => { + if (value === true || value === 'true') { + return true + } + + if (value === false || value === 'false') { + return false + } + + return typeof value === 'string' ? value : null +} + class UniwindStoreBuilder { runtime = UniwindRuntime vars = {} as Record private stylesheet = {} as StyleSheets private cache = {} as Record> + // The stylesheet doesn't change between reinits, so the data attributes a class string + // depends on are worked out once per class string + private dataAttributeNamesCache = new Map>() getStyles( className: string | undefined, @@ -40,7 +63,7 @@ class UniwindStoreBuilder { const isScopedTheme = uniwindContext.scopedTheme !== null const cacheKey = `${className}${state?.isDisabled ?? false}${state?.isFocused ?? false}${state?.isPressed ?? false}${isScopedTheme}${ uniwindContext.rtl ?? '' - }${uniwindContext.variables?.__uniwindVariablesCacheKey ?? ''}` + }${uniwindContext.variables?.__uniwindVariablesCacheKey ?? ''}${this.getDataAttributesCacheKey(className, componentProps)}` const cache = this.cache[uniwindContext.scopedTheme ?? this.runtime.currentThemeName] if (!cache) { @@ -53,19 +76,43 @@ class UniwindStoreBuilder { const result = this.resolveStyles(className, componentProps, state, uniwindContext) - // Don't cache styles that depend on data attributes - if (!result.hasDataAttributes) { - cache.set(cacheKey, result) - UniwindListener.subscribe( - () => cache.delete(cacheKey), - result.dependencies, - { once: true }, - ) - } + cache.set(cacheKey, result) + UniwindListener.subscribe( + () => cache.delete(cacheKey), + result.dependencies, + { once: true }, + ) return result } + private getDataAttributesCacheKey(className: string, componentProps: Record | undefined) { + let dataAttributeNames = this.dataAttributeNamesCache.get(className) + + if (dataAttributeNames === undefined) { + const names = new Set() + + for (const token of splitClassNames(className)) { + if (!(token in this.stylesheet)) { + continue + } + + for (const style of this.stylesheet[token] as Array