Per-instance config identity defeats the resolved-style cache (2307 entries where 417 suffice)
維護者通常 3 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 3/5
- 預估耗時
- 1-2 天
- 新手友好度
- 45/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 冷清
- 技術堆疊
- react-native, typescript
- 領域
- mobile, performance
研究方向
Start in src/native/api.tsx, comparing the module-scope mappingToConfig(mapping) in styled() with the per-instance useState call in useCssElement(), then check the identity-based cache key in src/native/react/rules.ts. Read src/components/View.tsx to see how the shipped wrappers pass a constant mapping. Done when repeated identical elements no longer add stylesFamily entries, and the existing native style tests still pass.
由索引模型根據 Issue 內容生成。
描述
Summary
useCssElement derives its style config per component instance, but the resolved-style cache is keyed on that config's object identity. Because every shipped components/*.tsx wrapper passes a module-constant mapping, N mounted elements produce N distinct config identities carrying identical values — so N identical elements each get their own stylesFamily entry, their own sorted rule array and their own observable, where one would do.
Measured on a real screen: 2307 cache entries where 417 suffice (5.5×).
styled() does not have this problem — it derives the config once at module scope. Both call sites are in the same file, ten lines apart.
Environment
react-native-css3.0.7 (currentlatest), and verified still present onmain- React Native 0.86, Fabric + Hermes, Expo SDK 57
- iPhone 12 Pro Max, iOS 26.6, release build
Steps to reproduce
stylesFamily is not reachable from any public export (./native re-exports api only; ./native-internal publishes root / style-collection / variables), so observing it needs a one-line instrumentation in the package's own source.
-
In
src/native/styles/index.ts, after the family is created:export const stylesFamily = family((hash, rules) => { /* unchanged */ }); // `family()` exposes `delete` / `keys` / `clear` and no `size`, so count via keys(). setInterval(() => { console.log("stylesFamily entries=" + [...stylesFamily.keys()].length); }, 1500); -
In any RN app built against that source, render many elements sharing one class list:
import { View } from "react-native"; // rewritten to components/View.tsx by the Metro resolver const Probe = () => ( <> {Array.from({ length: 150 }, (_unused, index) => ( <View key={index} className="size-5 rounded-full border bg-red-500" /> ))} </> ); -
Read the count once the tree has mounted.
Isolating comparison — swap the rewritten View for styled(RNView, { className: { target: "style" } }), which derives its config once at module scope, and re-read the count on the identical tree. Nothing else changes: same class list, same rules, same element type.
Expected result
The count reflects the number of distinct class lists on screen. 150 elements sharing one class list contribute one entry, because the cache exists to make resolution O(distinct class sets) rather than O(instances).
Actual result
The count scales with the number of instances.
On a screen of 150 identical controls (four styled elements each, plus surrounding chrome):
| build | stylesFamily entries |
|---|---|
3.0.7 as published |
2307 |
with mappingToConfig memoised on the mapping identity |
417 |
1890 redundant entries, each a separately sorted rule array and a retained observable over the same calculateProps, held for as long as the tree is mounted.
(417 rather than 1 is the honest floor for a real screen — it carries many distinct class lists, and e.g. ScrollView legitimately contributes two configs. What collapses is precisely the per-instance duplication.)
Root cause
The cache key (src/native/react/rules.ts:300) hashes the config by object identity, via a weakFamily that assigns a fresh number to each new object:
const keys = [state.configs, ...iterableKeys];
return generateHash(keys);
The rules half of that key shares correctly — StyleCollection.styles(className) is keyed by the class string.
The config half does not. In src/native/api.tsx, the two entry points disagree:
// line 54 — styled(): once, at module scope. Shares.
const configs = mappingToConfig(mapping);
// line 90 — useCssElement(): once per INSTANCE. Does not share.
const [config] = useState(() => mappingToConfig(mapping));
and every shipped wrapper takes the second path with a module-constant mapping (src/components/View.tsx:11,18):
const mapping = { className: { target: "style", resetsTextAncestor: true } };
// ...
return useCssElement(RNView, props, mapping);
mappingToConfig is pure in mapping, so it returns the same value every time and a different identity every time. The cache is defeated by an identity that always carries the same value.
Proposed fix
Memoise the derived config on the mapping identity — every call site the library itself ships already passes a stable module constant:
const MAPPING_CONFIG_CACHE = new WeakMap<object, Config[]>();
export function mappingToConfig(mapping) {
const cached = MAPPING_CONFIG_CACHE.get(mapping);
if (cached !== undefined) return cached;
const configs = /* existing body */;
MAPPING_CONFIG_CACHE.set(mapping, configs);
return configs;
}
Notes:
useCssElement'suseStatecan stay or go — withmappingToConfigmemoised it returns the shared array either way.- A caller passing a fresh mapping literal per render is unaffected (the
WeakMapsimply never hits), so this cannot make any call site worse. state.configsis read-only in its consumers (iterated forsource/target), so sharing one array across components is safe.
Impact, stated honestly
Two measurements, and they say different things:
- Entry count: unambiguous and large — 2307 → 417 on the screen above. The redundant work and the retained objects are real.
- CPU: small in our workload — one Instruments
--cpurun per side over a 160-instance stress scene showed the JS thread at 2914ms → 2794ms and 2129ms → 2082ms, which is inside our suite's run-to-run spread. So we can't claim a CPU win from our own numbers.
That bounds what removing it buys us, and says nothing about a workload with more distinct class sets, more configs per element, or a slower device. Reporting the defect and both numbers rather than the flattering one — you're better placed to judge what it's worth on the workloads the library targets.
Happy to open a PR if the approach looks right.
- 主要語言
- TypeScript
- 星號
- 166
- 分支
- 45
- 平均合併
- 13 天 14 小時
- 30 天內合併 PR
- 1
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 沒有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
nativewind/react-native-css 的其他 Issue
-
難度 1/5 1 小時以內 新手友好度 87/100
nativewind/react-native-css#506 ·
維護者通常 3 天內回覆
-
難度 5/5 一週以上 新手友好度 25/100
nativewind/react-native-css#510 ·
維護者通常 3 天內回覆
-
難度 3/5 1-2 天 新手友好度 56/100
nativewind/react-native-css#509 ·
維護者通常 3 天內回覆
-
難度 5/5 一週以上 新手友好度 25/100
nativewind/react-native-css#507 ·
維護者通常 3 天內回覆
-
難度 3/5 1-2 天 新手友好度 62/100
nativewind/react-native-css#481 · 1 個 reaction ·
維護者通常 3 天內回覆
查看 nativewind/react-native-css 的全部 Issue
相似的 Issue
-
難度 2/5 1-3 小時 新手友好度 76/100
Graphify-Labs/graphify#4860 · 1 則留言 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 72/100
sveltejs/eslint-plugin-svelte#1615 ·
維護者通常 7 天內回覆
-
Parallel reticle_verify opens per-flow leases without the source lease's ignoreHTTPSErrors可能已有人在做 @as992949791 今天認領。 未關閉good first issue help wanted
難度 2/5 1-3 小時 新手友好度 85/100
reticlehq/reticle#2033 · 1 則留言 ·
維護者通常 1 天內回覆
-
bug
難度 2/5 1-3 小時 新手友好度 68/100
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 1 天內回覆