Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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`

未关闭
#546 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 3 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
react, react-native, typescript

调研方向

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
  1. Clone the reproducer branch, which is main at 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
    
  2. 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 模板
  • 阅读贡献指南

从这里开始

  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 摘要。