Every styled element costs about 2.8x a React Native one to mount: a wrapper component, a rule pass and effects, with or without a `className`
维护者通常 3 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 48/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- react, react-native, typescript
- 领域
- mobile, performance
调研方向
Start with src/components/View.tsx, src/native/api.tsx, and src/native/react/useNativeCss.ts, which the issue identifies as the wrapper and mount-time work. Read the mount-cost.test.tsx reproducer on branch repro/546-per-element-mount-cost and run it with yarn jest src/__tests__/native/mount-cost.test.tsx. Done means elements without a className skip the rule pass and effects, while styled elements mount near the cost of their React Native counterparts.
由索引模型根据 Issue 内容生成。
描述
Before submitting a new issue
- I tested using the latest version of the library, as the bug might be already fixed.
- I tested using a supported version of react native.
- I checked for possible duplicate issues, with possible answers.
Bug summary
Each styled component is a wrapper that calls useCssElement(Component, props, mapping) (src/components/View.tsx and its siblings) and returns createElement(Component, props), so every element mounts one component instance more than the component it styles. Every element then pays at mount, with a className or without one: useCssElement's useState(() => mappingToConfig(mapping)) (src/native/api.tsx), two useContext reads in useNativeCss (src/native/react/useNativeCss.ts), a useState initializer that builds two Effect objects, each holding a Set, and runs updateRules, a useRef and a passive useEffect, and testGuards and getStyledProps on every render.
A styled View takes about 2.8x a React Native View to mount in this harness, and as much without a className as with one, since the rule pass runs once per element either way.
Expected: an element without a className costs what the component it wraps costs, and one with a className pays the rule pass and nothing it does not use.
Library version
main at 2421ea7 (0.0.0-nightly.2421ea7)
Environment info
System:
OS: Windows 11 10.0.26300
CPU: "(32) x64 AMD Ryzen 9 9950X3D 16-Core Processor "
Memory: 155.26 GB / 255.67 GB
Binaries:
Node:
version: 26.12.1
path: C:\Program Files\nodejs\node.EXE
Yarn:
version: 4.9.2
path: C:\Users\<user>\AppData\Roaming\npm\yarn.CMD
npm:
version: 12.2.1
path: C:\Program Files\nodejs\npm.CMD
Watchman: Not Found
SDKs:
Android SDK:
API Levels:
- "33"
- "35"
- "35"
- "35"
- "36"
- "37"
Build Tools:
- 35.0.0
- 36.0.0
- 36.1.0
- 37.0.0
System Images:
- android-26 | Google APIs Intel x86_64 Atom
- android-28 | Google APIs Intel x86_64 Atom
- android-29 | Google APIs Intel x86_64 Atom
- android-30 | Google APIs Intel x86_64 Atom
- android-31 | Google APIs Intel x86_64 Atom
- android-32 | Google APIs Intel x86_64 Atom
- android-33 | Google APIs Intel x86_64 Atom
- android-34 | Google APIs Intel x86_64 Atom
- android-35 | Google APIs Intel x86_64 Atom
- android-36.1 | Google Play Intel x86_64 Atom
- android-36.1 | Pre-Release 16 KB Page Size Google Play Intel x86_64 Atom
- android-37.0 | Google APIs Intel x86_64 Atom
Android NDK: Not Found
Windows SDK: Not Found
IDEs:
Android Studio: AI-262.9437.185.2621.16467767
Visual Studio: Not Found
Languages:
Java:
version: 17.0.20.1
path: C:\Program Files\Microsoft\jdk-17.0.20.101-hotspot\bin\javac.EXE
Ruby: Not Found
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.86.3
wanted: 0.86.3
react-native-windows: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
Steps to reproduce
- Clone the reproducer branch, which is
mainat 2421ea7 with one file added,src/__tests__/native/mount-cost.test.tsx, and install:git clone --branch repro/546-per-element-mount-cost https://github.com/YevheniiKotyrlo/react-native-css cd react-native-css yarn install - Run
yarn jest src/__tests__/native/mount-cost.test.tsx.
src/__tests__/native/mount-cost.test.tsx:
import { View as RNView, type ViewProps } from "react-native";
import { performance } from "node:perf_hooks";
import { render } from "@testing-library/react-native";
import { View } from "react-native-css/components/View";
import { registerCSS } from "react-native-css/jest";
import * as rules from "../../native/react/rules";
const ELEMENTS = 500;
const WARM_UP_RUNS = 1;
const RUNS = 25;
const ARMS = [
"react-native View",
"react-native View in a function component",
"css View, no className",
"css View, className",
] as const;
type Arm = (typeof ARMS)[number];
const STYLE = { backgroundColor: "red", padding: 8 } as const;
function WrappedView(props: ViewProps) {
return <RNView {...props} />;
}
function renderArm(arm: Arm) {
const children = Array.from({ length: ELEMENTS }, (_, index) => {
switch (arm) {
case "react-native View":
return <RNView key={index} style={STYLE} />;
case "react-native View in a function component":
return <WrappedView key={index} style={STYLE} />;
case "css View, no className":
return <View key={index} style={STYLE} />;
case "css View, className":
return <View key={index} className="box" />;
}
});
return render(<RNView>{children}</RNView>);
}
/** react-test-renderer's instance tree holds composite and host components alike. */
interface ComponentInstance {
readonly children: readonly (string | ComponentInstance)[];
}
function countComponentInstances(instance: ComponentInstance): number {
return instance.children.reduce<number>(
(count, child) =>
typeof child === "string"
? count
: count + countComponentInstances(child),
1,
);
}
function median(values: readonly number[]): number {
const sorted = [...values].sort((left, right) => left - right);
return sorted[Math.floor(sorted.length / 2)] ?? Number.NaN;
}
beforeEach(() => {
registerCSS(`.box { background-color: red; padding: 8px; }`);
});
afterEach(() => {
jest.restoreAllMocks();
});
test("a css View mounts one more component instance than a react-native View", () => {
const instancesOf = (arm: Arm): number => {
const result = renderArm(arm);
const count = countComponentInstances(result.UNSAFE_root);
result.unmount();
return count;
};
const reactNative = instancesOf("react-native View");
const wrapped = instancesOf("react-native View in a function component");
const cssWithoutClassName = instancesOf("css View, no className");
const cssWithClassName = instancesOf("css View, className");
console.log(
[
`react-native View: ${String(reactNative)} instances for ${String(ELEMENTS)} elements`,
`react-native View in a function component: ${String(wrapped)}`,
`css View, no className: ${String(cssWithoutClassName)}`,
`css View, className: ${String(cssWithClassName)}`,
].join("\n"),
);
expect(wrapped).toBe(reactNative + ELEMENTS);
expect(cssWithoutClassName).toBe(reactNative + ELEMENTS);
expect(cssWithClassName).toBe(reactNative + ELEMENTS);
});
test("every css View runs the rule pass when it mounts, className or not", () => {
const updateRules = jest.spyOn(rules, "updateRules");
const rulePassesOf = (arm: Arm): number => {
updateRules.mockClear();
renderArm(arm).unmount();
return updateRules.mock.calls.length;
};
const withoutClassName = rulePassesOf("css View, no className");
const withClassName = rulePassesOf("css View, className");
console.log(
[
`css View, no className: ${String(withoutClassName)} rule passes for ${String(ELEMENTS)} elements`,
`css View, className: ${String(withClassName)} rule passes for ${String(ELEMENTS)} elements`,
].join("\n"),
);
expect(withoutClassName).toBe(ELEMENTS);
expect(withClassName).toBe(ELEMENTS);
});
test("the mount cost per element, react-native View against css View", () => {
const samples: Record<Arm, number[]> = {
"react-native View": [],
"react-native View in a function component": [],
"css View, no className": [],
"css View, className": [],
};
for (let run = 0; run < WARM_UP_RUNS + RUNS; run += 1) {
const rotation = run % ARMS.length;
for (const arm of [...ARMS.slice(rotation), ...ARMS.slice(0, rotation)]) {
const start = performance.now();
const result = renderArm(arm);
const elapsed = performance.now() - start;
result.unmount();
if (run >= WARM_UP_RUNS) {
samples[arm].push(elapsed);
}
}
}
const baseline = median(samples["react-native View"]);
console.log(
[
`mounting ${String(ELEMENTS)} elements, median of ${String(RUNS)} runs (Node ${process.version}, NODE_ENV=${process.env.NODE_ENV}, arm order rotated per run):`,
...ARMS.map((arm) => {
const milliseconds = median(samples[arm]);
const perElement = (milliseconds * 1000) / ELEMENTS;
const spread = `${Math.min(...samples[arm]).toFixed(1)}-${Math.max(...samples[arm]).toFixed(1)} ms`;
return ` ${arm}: ${milliseconds.toFixed(1)} ms (${spread}), ${perElement.toFixed(1)} µs per element, ${(milliseconds / baseline).toFixed(2)}x`;
}),
].join("\n"),
);
for (const arm of ARMS) {
expect(samples[arm]).toHaveLength(RUNS);
}
});
Expected: an element mounts at close to the cost of the React Native component it styles, and one without a className skips the rule pass and its effects.
Actual: every element mounts a wrapper, runs a rule pass and effects, with or without a className, at about 2.8 times a React Native element's cost.
The run's output on main at 2421ea7, Windows 11, Node 26.12.1
console.log
react-native View: 1002 instances for 500 elements
react-native View in a function component: 1502
css View, no className: 1502
css View, className: 1502
at Object.log (src/__tests__/native/mount-cost.test.tsx:86:11)
console.log
css View, no className: 500 rule passes for 500 elements
css View, className: 500 rule passes for 500 elements
at Object.log (src/__tests__/native/mount-cost.test.tsx:110:11)
console.log
mounting 500 elements, median of 25 runs (Node v26.12.1, NODE_ENV=test, arm order rotated per run):
react-native View: 3.8 ms (3.7-7.1 ms), 7.6 µs per element, 1.00x
react-native View in a function component: 5.1 ms (4.9-14.0 ms), 10.2 µs per element, 1.34x
css View, no className: 8.5 ms (8.0-25.0 ms), 17.1 µs per element, 2.25x
css View, className: 12.6 ms (9.3-29.0 ms), 25.2 µs per element, 3.32x
at Object.log (src/__tests__/native/mount-cost.test.tsx:143:11)
PASS src/__tests__/native/mount-cost.test.tsx
√ a css View mounts one more component instance than a react-native View (95 ms)
√ every css View runs the rule pass when it mounts, className or not (41 ms)
√ the mount cost per element, react-native View against css View (961 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 2.116 s, estimated 3 s
Ran all test suites matching /src\\__tests__\\native\\mount-cost.test.tsx/i.
Reproducible example repository
https://github.com/YevheniiKotyrlo/react-native-css/tree/repro/546-per-element-mount-cost
- 主要语言
- TypeScript
- 星标
- 166
- 派生
- 45
- 平均合并
- 13 天 14 小时
- 30 天内合并 PR
- 1
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
nativewind/react-native-css 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 78/100
nativewind/react-native-css#545 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
nativewind/react-native-css#544 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
nativewind/react-native-css#543 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 84/100
nativewind/react-native-css#540 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
nativewind/react-native-css#538 ·
维护者通常 3 天内回复
查看 nativewind/react-native-css 的全部 Issue
相似的 Issue
-
area:desktop area:ui bug platform:macos
难度 2/5 1-3 小时 新手友好度 67/100
anthropics/claude-code#106183 ·
维护者通常 1 天内回复
-
难度 1/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复
-
feature:App-Tour feature:Chat ready Web
难度 2/5 1-3 小时 新手友好度 62/100
digitalfabrik/integreat-app#4523 · 2 个 reaction ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
Required Lexical field accepts two empty paragraphs可能已有人在做 @lazerg 今天认领。 未关闭Bug
难度 2/5 1-3 小时 新手友好度 76/100
payloadcms/payload#19222 ·
维护者通常 1 天内回复