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`
Les mainteneurs répondent en général sous 3 jours
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 48/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Stack technique
- react, react-native, typescript
- Domaine
- mobile, performance
Piste de recherche
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.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
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
- Langage dominant
- TypeScript
- Étoiles
- 166
- Forks
- 45
- Merge moyen
- 13 j 14 h
- PR mergées (30 j)
- 1
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Aucun modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de nativewind/react-native-css
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
nativewind/react-native-css#545 ·
Les mainteneurs répondent en général sous 3 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
nativewind/react-native-css#544 ·
Les mainteneurs répondent en général sous 3 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
nativewind/react-native-css#543 ·
Les mainteneurs répondent en général sous 3 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
nativewind/react-native-css#540 ·
Les mainteneurs répondent en général sous 3 jours
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
nativewind/react-native-css#538 ·
Les mainteneurs répondent en général sous 3 jours
Toutes les issues de nativewind/react-native-css
Issues similaires
-
bug
Difficulté 1/5 Moins d'une heure Accessibilité débutants 86/100
awslabs/visual-asset-management-system#937 ·
Les mainteneurs répondent en général sous 1 jour
-
t0ggles
Difficulté 1/5 Moins d'une heure Accessibilité débutants 82/100
nolimits4web/swiper#8255 · 1 commentaire ·
Les mainteneurs répondent en général sous 15 jours
-
render: --variables-file (and --variables <path>) fails with "io.readFile is not a function" since 0.8.212Peut-être pris @miguel-heygen l’a pris aujourd’hui. Ouvertebug triage/needs-triage
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
heygen-com/hyperframes#6830 ·
Les mainteneurs répondent en général sous 1 jour
-
bug launch local use
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
Les mainteneurs répondent en général sous 1 jour
-
account: profile bug
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 5 jours