Skip to content

Performance: theme color computations run on every render in List.Item, TextInput, Chip, Divider, Appbar.Action #4946

@ayobamiseun

Description

@ayobamiseun

Current behaviour

Several theme-derived colors in commonly-rendered components are recomputed via the color package (color(...).alpha(...).rgb().string()) on every render, even when their inputs don't change across renders. The result is wasted CPU on every keystroke in TextInput, on every parent re-render of any list of List.Item / Chip / Divider, and on every render of Appbar.Action. We noticed it while profiling typing latency on a search screen and scroll/interaction jank on a settings list, where calls into the color package showed up much more frequently than expected in flamegraphs.

The affected sites we found (all in the v2 theme path or other input-independent branches):

  • src/components/Divider.tsx:67-70 — color(isDarkTheme ? white : black).alpha(0.12).rgb().string(). The inputs are constants plus a boolean; there are exactly two possible results, but we recompute on every render of every divider.

  • src/components/List/ListItem.tsx:209-211 and 233-235 — titleColor and descriptionColor run a full color() chain on every render. List.Item is rendered N times in lists, so the cost multiplies.

  • src/components/Appbar/AppbarAction.tsx:108 — color(black).alpha(0.54).rgb().string(). This branch has no dependencies at all; both arguments are constants, yet the chain runs on every render.

  • src/components/TextInput/helpers.tsx:332, 367, 401, 410, 421-422 — getTextColor, getActiveColor, getSelectionColor, getFlatBackgroundColor each contain color() chains and are called on every TextInput render. Since TextInput re-renders on every keystroke, this fires constantly during typing.

  • src/components/Chip/helpers.tsx:34, 38, 46, 50, 53, 89, 92, 135 — getBorderColor, getTextColor, getBackgroundColor each contain multiple color().alpha().rgb().string() branches and run per Chip per render. A filter strip with N chips multiplies this by N on every parent re-render.

Individually each call is cheap, but the color package parses + allocates + serializes on each invocation, and these are all in render paths that fire either per-keystroke or per-list-item.

Expected behaviour

Theme-derived colors that don't change between renders should be computed once (module-level constant when the inputs are constants, or useMemo keyed on the actual dependencies) rather than on every render.

Concretely:

  • Divider v2 branch → two module-level constants (DARK_DIVIDER_COLOR, LIGHT_DIVIDER_COLOR), pick between them.

  • Appbar.Action v2 fallback → module-level constant for color(black).alpha(0.54).rgb().string().

  • List.Item titleColor / descriptionColor → useMemo keyed on theme.colors.text and theme.isV3.

  • TextInput helpers → memoize the helper results at the call site (keyed on theme, disabled, error, mode, custom colors), or cache by argument tuple inside the helpers.

  • Chip helpers → one useMemo in Chip for the three derived colors, keyed on theme, isOutlined, disabled, selectedColor.

No behavior change, no API change — purely moving constants out of render scope and adding useMemo where dependencies are stable.

How to reproduce?

This isn't a visual bug, so the "repro" is profiling rather than a screenshot. To observe it directly:

In any app using react-native-paper, open a screen with a TextInput from the library.
Start the React DevTools Profiler (or use Flipper / Hermes sampling profiler) and record while typing a few characters.
Inspect the flame graph for the TextInput commit cycle — calls into the color package (alpha, rgb, string) appear on every keystroke from getTextColor, getActiveColor, getSelectionColor, and (in flat mode) getFlatBackgroundColor.
Same pattern reproduces with a list of ~30 List.Items, or a FlatList of Chips, by triggering a parent re-render (e.g. toggling a selection) and recording the commit.

A minimal reproduction isn't strictly necessary here since the issue is visible by reading the source at the linked line numbers — happy to put one together on Snack if it would help maintainers.

Preview

N/A — profiler-observable, not visually observable. Can attach a flamegraph screenshot if maintainers want one.

What have you tried so far?

What have you tried so far?
Patched the Divider and Appbar.Action v2 branches locally to use module-level constants — verified via the profiler that the color calls disappear from those components' commits, no visual regression.
Wrapped titleColor / descriptionColor in List.Item in useMemo keyed on theme.colors.text and theme.isV3 — same result, no regression.
For TextInput, memoizing the four helper results at the call site removed the per-keystroke color calls in the profile.
All changes are local and mechanical; happy to send a PR (combined or split per component, whichever the maintainers prefer).

Your Environment

software version
ios 18
android x
react-native 0.82.1
react-native-paper 5.15.2
node 22.x.x
npm or yarn 11.12.1
expo sdk 53

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions