Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

Per-instance config identity defeats the resolved-style cache (2307 entries where 417 suffice)

未關閉
#433 2 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 3 天內回覆

還沒有人認領這個 Issue。

評估

難度
3/5
預估耗時
1-2 天
新手友好度
45/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
冷清
技術堆疊
react-native, typescript

研究方向

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 內容生成。

描述

auto-triaged bug confirmed

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-css 3.0.7 (current latest), and verified still present on main
  • 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.

  1. 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);
    
  2. 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" />
        ))}
      </>
    );
    
  3. 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's useState can stay or go — with mappingToConfig memoised it returns the shared array either way.
  • A caller passing a fresh mapping literal per render is unaffected (the WeakMap simply never hits), so this cannot make any call site worse.
  • state.configs is read-only in its consumers (iterated for source / 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 --cpu run 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 範本
  • 閱讀貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

nativewind/react-native-css 的其他 Issue

查看 nativewind/react-native-css 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。