# Getting Started
## Introduction [#introduction]
React Native Boost consists of two pieces:
* A Babel plugin that statically analyzes your source code and replaces safe `Text`, `View`, and `Image` components with their direct native counterparts, leading to significant performance improvements compared to the JS-based wrapper components.
* A runtime package used internally by the plugin for cross-platform-safe imports and helper utilities.
The analyzer is intentionally strict and skips any optimizations that may lead to user-facing bugs and behavioral changes.
## Compatibility [#compatibility]
| `react-native-boost` | React Native |
| -------------------- | ---------------- |
| `0.x` | All versions[^1] |
| `1.x` | `>=0.83` |
[^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings. [See react-native-community/discussions-and-proposals #893.](https://github.com/react-native-community/discussions-and-proposals/discussions/893)
## Getting Started [#getting-started]
1. Install React Native Boost:
```bash
npm install react-native-boost
```
```bash
pnpm add react-native-boost
```
```bash
yarn add react-native-boost
```
```bash
bun add react-native-boost
```
2. If you use Expo and do not have a `babel.config.js` yet:
```bash
npx expo customize babel.config.js
```
3. Add the plugin:
```js
// babel.config.js
module.exports = {
plugins: ['react-native-boost/plugin'],
};
```
If you're using Unistyles or Nativewind in your project, refer to these additional setup instructions:
4. Restart the development server and clear cache:
```bash
npm start -- --clear
```
```bash
pnpm start -- --clear
```
```bash
yarn start --clear
```
```bash
bun run start --clear
```
The Babel plugin imports optimized components via `react-native-boost/runtime`, so `react-native-boost` must be
available at runtime and must therefore **not** be installed as a dev dependency.
## Platform Support [#platform-support]
React Native Boost supports all platforms. Optimizations are performed on iOS and Android, while falling back to the default components on all other platforms.
# Nativewind Support
Compatibility and setup depends on your Nativewind version.
## Nativewind v4 [#nativewind-v4]
Register Boost's native components with `cssInterop` at your app's entry point, before anything renders. Without this setup, Boost-optimized components lose all `className` styling.
```jsx
import { cssInterop } from 'nativewind';
import { NativeText, NativeView } from 'react-native-boost/runtime';
cssInterop(NativeText, { className: 'style' });
cssInterop(NativeView, { className: 'style' });
```
### Known limitations [#known-limitations]
A few Tailwind classes map to props that the JS `Text` wrapper used to translate. Boost skips that
wrapper, so apply these via `style` instead of `className`:
| Avoid | Use |
| -------------------------- | ------------------------------------------------ |
| `className="select-auto"` | `selectable` or `style={{ userSelect: 'auto' }}` |
| `className="align-center"` | `style={{ textAlignVertical: 'center' }}` |
The `style` forms work because Boost translates them for you; the same values from `className` reach the native
host raw and are invalid.
## Nativewind v5 [#nativewind-v5]
Technical feasibility for support is still being investigated.
# Unistyles Support
React Native Boost is compatible with react-native-unistyles v3. Previous versions of Unistyles are untested and not officially supported.
## Setup [#setup]
Enable Boost's Unistyles support layer through the Babel plugin's config options:
```js
// babel.config.js
module.exports = {
plugins: [
['react-native-boost/plugin', { unistyles: true }],
['react-native-unistyles/plugin', { root: 'src' }],
],
};
```
Set `unistyles: false` to turn the mode off, e.g. when Unistyles is installed in your project's dependencies, but not actually used in code. The order of the two plugins does not matter.
React Native Boost can auto-detect Unistyles and enable this mode automatically if you haven't explicitly disabled it. However, this auto-detection is fragile and Boost will therefore log a warning to the console. Explicitly set the config flag as shown above to silence the warning.
## How it works [#how-it-works]
Unistyles updates styles natively, outside of React.
In Unistyles mode, Boost looks at each `Text`/`View`'s `style` and routes accordingly:
* **A Unistyles style** (from `StyleSheet.create` imported from `react-native-unistyles`) → rewritten to Unistyles' own lean host, keeping Unistyles' reactivity, while still providing Boost's performance benefits.
* **A plain React Native style** (an object literal, or a `StyleSheet.create` from `react-native`) →
optimized to Boost's standard native host, exactly as in a non-Unistyles app.
* **A style Boost can't resolve** (e.g. `style={props.style}`, a function call, a conditional) → left
untouched. When Boost can't reliably tell if it's a Unistyles style arriving from elsewhere or a plain style object, it has to skip it. When Unistyles mode is disabled, this does not apply, and all components (that don't bail for other reasons) are optimized, no matter where their `style` comes from.
### Known limitations [#known-limitations]
The native components React Native Boost rewrites your components to don't perform some of the prop and style processing the standard JS-based wrapper components do. Without Unistyles, React Native Boost can do this processing for you. With Unistyles, this isn't possible, unfortunately. Therefore:
| Avoid | Use |
| ---------------------------- | ------------------------------------------------ |
| `fontWeight: 700` (a number) | `fontWeight: '700'` (a string) |
| `userSelect: 'none'` | the `selectable` prop, e.g. `selectable={false}` |
| `verticalAlign: 'middle'` | `textAlignVertical: 'center'` |
Boost forwards a Unistyles style to the native host untouched (this is what preserves Unistyles'
reactivity), so the raw forms reach the host as-is. The right-hand values are already in their native
form, so they work whether or not Unistyles is in play. These only affect `Text`.
# Uniwind Support
React Native Boost and [Uniwind](https://uniwind.dev) are currently incompatible. Enabling Boost in a Uniwind project drops `className` styling from optimized components entirely.
## What happens [#what-happens]
Uniwind resolves `className` to styles inside its own `Text`/`View` wrappers, which it swaps in via a
Metro resolver. Boost rewrites `Text`/`View` to their native counterparts **before** that swap, so `className` is left as an unknown prop on the raw native component.
## What breaks [#what-breaks]
Any `Text`/`View` with a `className` (or a `*ClassName` prop such as `selectionColorClassName`):
* All Tailwind styling is dropped, the element renders with no styles
* Uniwind extras are lost too (press state, `selectionColorClassName`, line-clamp)
## Roadmap [#roadmap]
Feasibility for Uniwind support is still being investigated.
# Decorators
## @boost-ignore [#boost-ignore]
Use `@boost-ignore` to disable optimization on a specific element.
If a line containing `@boost-ignore` appears immediately before a JSX opening tag, that component is skipped.
```jsx
This will be optimized.
{/* @boost-ignore */}
This will not be optimized.
```
## @boost-force [#boost-force]
Use `@boost-force` to force optimization on a specific element, even if it would normally be skipped by a bailout rule (e.g. blacklisted props, unresolvable spreads, or ancestor checks).
The only check that `@boost-force` does **not** override is the `react-native` import check — the component must still be imported from `react-native`.
```jsx
const Component = ({ props }) => {
return (
{/* @boost-force */}
This will be optimized despite having unresolvable spread props.
)
}
```
`@boost-force` bypasses safety checks that exist to prevent behavioral changes.
Only use it when you are confident that the optimization is safe for your specific use case.
# Configure the Babel Plugin
The Babel plugin (`react-native-boost/plugin`) is the core of React Native Boost.
Defaults are safe and usable out of the box, but you can tune behavior for your app.
## Example Configuration [#example-configuration]
```js
// babel.config.js
module.exports = {
plugins: [
[
'react-native-boost/plugin',
{
verbose: false,
silent: false,
unistyles: false,
ignores: ['node_modules/**'],
optimizations: {
text: true,
view: true,
image: true,
},
},
],
],
};
```
## Plugin Options [#plugin-options]
## Plugin Optimization Options [#plugin-optimization-options]
## Environment-Specific Enablement [#environment-specific-enablement]
You can enable React Native Boost by environment with Babel `env` config:
```js
module.exports = {
env: {
development: {
plugins: ['react-native-boost/plugin'],
},
},
};
```
See Babel docs: [https://babeljs.io/docs/options#env](https://babeljs.io/docs/options#env)
# Benchmarks
To show what that React Native Boost buys you,
the example app in the repository serves as a benchmark by rendering a heavy, constantly-updating screen and
measuring the **frame rate**. We compare three configurations on the exact same device: the
**baseline** (stock React Native), a [**core-optimized** configuration](#reducedefaultpropsintext-feature-flag-in-react-native) that turns on React Native's own
overhead-reduction feature flag (detailed below), and **React Native Boost**.
Higher frames per second (FPS) means a smoother UI. The ideal target is 60 FPS, which means you need to commit one new frame every \~16 ms. Once the app can't keep up, FPS drops and the interface starts to stutter.
## Results [#results]
**iOS**: The baseline starts dropping frames early and falls to \~32 FPS under the heaviest load, while React Native Boost
holds a solid 60 FPS far longer and stays roughly **69% faster** at the top end. The core-optimized build (the middle
line) recovers part of the gap — around **8–15%** under heavy load — but Boost stays roughly **50% ahead of even
that**.
**Android**: Same shape, with Boost about **55% faster** at the heaviest load (and higher at intermediate heavy loads).
The core-optimized build helps here too — roughly **10–22%** at heavy loads. This device is noisy enough that a handful
of loads couldn't be measured reliably for the core comparison and are left out of that middle line (you'll see small
gaps), but Boost's lead is unambiguous throughout.
## Methodology [#methodology]
The test screen is a live crypto-style order book: two stacked columns of rows, each
row a few pieces of `Text`, all updating many times per second from a simulated price feed.
Each run is a clean comparison on identical inputs:
1. **One build, three configurations.** From a single release build we run the *baseline* (stock React Native), a *core-optimized* configuration with React Native's `reduceDefaultPropsInText` flag enabled, and *React Native Boost*. Nothing else changes.
2. **Sweep the load.** Each configuration steps through a range of row counts, from light to heavy.
3. **Hold the temperature constant.** Phones throttle as they heat up, which would corrupt the comparison. So before every capture the app idles until the device cools back to a fixed thermal floor, and every sample records the device's thermal state. All three series are measured at the same temperature.
4. **Measure FPS, repeatedly.** The app runs a continuous animation loop and records how long every frame takes over a fixed window, reporting the average FPS (plus 95th-percentile frame time and the share of dropped frames). Each point is captured several times and reported as the **median**, with the replicate spread drawn as error bars on the graphs.
5. **Validate the core comparison.** Boost's frame rate doesn't depend on the `reduceDefaultPropsInText` flag, so the Boost curve should land identically in the baseline and core-optimized runs. This is used as a built-in sanity check / validator. At any load where the two disagree (or the device is simply too noisy to trust), the point is dropped as unreliable.
Everything runs as a **release/production build on the New Architecture**.
## Secondary benchmarks [#secondary-benchmarks]
### Number of React tree nodes [#number-of-react-tree-nodes]
We're also making a second, device-independent measurement. For a single row, we count the **React tree nodes**
(fibers). One of the ways React Native Boost impacts an app's performance is by deleting a layer of these fibers per element it optimizes.
Fewer nodes means less for React to build and re-check on every frame.
### Time-to-mount [#time-to-mount]
For previous versions of React Native, we also published benchmarks showing heavy performance improvements in initial render (mounting) times. `Text` components optimized through React Native Boost rendered up to 50% faster on iOS, and around 20% faster on Android, compared to the baseline standard `Text` component.
Thanks to various improvements to the reconciler and other internals in React Native 0.78, this advantage has shrunken to only around \~4-6% in recent benchmarks. React Native Boost remains advantageous for frequently-updated screens, less so for screens with only a lot of static components.
## `reduceDefaultPropsInText` feature flag in React Native [#reducedefaultpropsintext-feature-flag-in-react-native]
In React Native 0.82, a `reduceDefaultPropsInText` feature flag was introduced. It trimmed the `Text` wrapper's output so that unset accessibility/`aria-*` props no longer crossed the JS→native boundary on every render. It shipped disabled by default and has since **graduated**: in newer React Native the flag is gone and the optimized behavior is simply the default. It's one step in React Native's long-term goal of reducing the `Text` and `View` wrapper overhead directly in core. The [Deep Dive](/docs/information/deep-dive#react-natives-own-roadmap) traces its full lifecycle.
In our benchmark, the **core-optimized** series in the graphs enables `reduceDefaultPropsInText` (on the React Native version measured here, where the flag still exists). Under heavy render load it recovers roughly **8–15%** of the baseline's lost frame rate on iOS (a bit more on Android), but closes only part of the gap. `NativeText`, the component React Native Boost uses under the hood, stays well ahead — around a **50%** margin over the core-optimized build at the heaviest loads on iOS. As React Native lands more of these optimizations, we expect the core-optimized line to keep rising toward React Native Boost.
# Technical Deep Dive
This page is the long version of [How It Works](/docs/information/how-it-works). It walks through what the
`Text` and `View` wrappers actually do on every render, traces a single element from JSX all the way to
a native shadow node, and then shows exactly how the Babel plugin and its runtime helpers collapse that
work safely.
This has last been updated for React Native 0.85 and React Native Boost 1.3.0.
## `Text` and `View` are not host components [#text-and-view-are-not-host-components]
It's tempting to think of `Text` and `View` as native platform primitives. Surprisingly (even to many senior RN developers), they aren't. They are ordinary JavaScript **function components** that each render a
lower-level component underneath:
* `Text` renders `NativeText` (the host component `RCTText`), or `NativeVirtualText` (`RCTVirtualText`) when nested inside another `Text`.
* `View` renders `ViewNativeComponent` (the host component `RCTView`).
Those underlying components are the real primitives. At runtime they resolve to the plain string `'RCTText'` and `'RCTView'` respectively, and React reconciles that to a **host component**. It builds the shadow node directly, with no JavaScript component fiber in between.
`NativeComponentRegistry.get(...)` registers the view config and **returns the name string**. `ViewNativeComponent` is literally `'RCTView'`, the same is true of `NativeText`:
The wrapper exists to translate ergonomic, cross-platform props into the props the host component actually
understands, and to manage some JS-side state (specifically, `TextAncestorContext` and pressability). The crucial
detail is that **all of that translation runs in JavaScript, on every render**, even when your element uses none
of it. They are rare edge cases producing real overhead for *every* element.
## Itemizing this tax [#itemizing-this-tax]
### `Text` [#text]
The `Text` wrapper is a single 550+ line file. Each time React renders a ``, its function body runs top to bottom.
Even a bare `Hello` pays for:
* A **function-component invocation** and the React fiber that backs it.
* **Destructuring \~35 named props** out of `props`; every one a property read, most resolving to `undefined`.
* **`aria-*` → `accessibility*` translation**: an `aria-label` coalesce, plus a five-field `accessibilityState` merge that allocates a fresh object whenever any `aria-*` state prop is set.
* **`disabled` ↔ `accessibilityState.disabled` reconciliation**.
* A **`Platform.select`** to resolve the default `accessible` value.
* An **`isPressable`** computation and `accessibilityRole`/`role` link defaulting.
* **`processColor(selectionColor)`** when a selection color is set.
* A **`numberOfLines` clamp** (with a dev-only `console.error` for negatives).
* A **`flattenStyle` walk** of the `style` prop, followed by `fontWeight` number→string conversion, `userSelect` lookup, and `verticalAlign` → `textAlignVertical` mapping, each potentially allocating an `overrides` object and a new `[style, overrides]` array:
* A **`useContext(TextAncestorContext)`** subscription, used to decide whether to render as `RCTText` or `RCTVirtualText`. Once a fiber subscribes to a context, React must re-render it whenever that context value changes.
* A decision about whether to wrap the output in a **`` provider** which, when installed, is itself an extra fiber with its own context push/pop in the commit phase:
Importantly, none of this is expensive individually. The problem is the multiplier. A list row might contain a dozen
`Text` elements, a screen a few hundred. And the tax is paid by
every single one, on every single UI commit.
### `View` [#view]
The `View` wrapper is a lot leaner. Still, on each render it subscribes to `TextAncestorContext`, destructures 17 named props, runs a series of
`aria-*` translations, and decides whether to flip the context back to `false` for its descendants:
## From JSX to pixels: the call graph [#from-jsx-to-pixels-the-call-graph]
Consider `Hello`. Here is the path it travels, with and without Boost.
Without Boost, React sees a **function** type and renders a component fiber. It has to run everything in the
section above before the wrapper can even return the `RCTText` element. With Boost, React skips straight to the host fiber. Everything between "wrapper fiber" and "host fiber" disappears.
The host side is identical either way. A single `Hello` always produces **two** shadow
nodes: a `ParagraphShadowNode` and a `RawTextShadowNode` child for the string (mounted as the `RCTText` and `RCTRawText` views).
Boost doesn't touch any of that. It only removes the JavaScript that ran *before* the host node was ever
created.
The same is true of the React tree. Boost deletes the wrapper fiber (and, where the wrapper would have
added one, the `TextAncestorContext` provider fiber):
Fewer fibers means less for React to build, diff, and commit on every frame (something our
[React tree node benchmark](/docs/information/benchmarks#number-of-react-tree-nodes) measures).
## What Boost emits [#what-boost-emits]
Boost's core move is to swap the JSX element's type from `Text`/`View`/`Image` to
`NativeText`/`NativeView`/`NativeImage` (imported from `react-native-boost/runtime`), after reproducing
(at *build time*) whatever inescapable work the wrapper would have done for the specific element.
### Text [#text-1]
The simplest case bakes in the defaults the wrapper would have applied, too:
```tsx
// in
Hello
// out
import { getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}>Hello;
```
`accessible` is
platform-specific (`true` on iOS, `false` on Android, omitted on web). When Metro tells Boost which
platform it's bundling for at compile time, the literal is inlined directly (`accessible={true}`); *only when it doesn't* (though it usually does!) it falls
back to the tiny [`getDefaultTextAccessible()`](/docs/runtime-library) runtime helper shown above. `allowFontScaling` and `ellipsizeMode` are simply the wrapper's defaults, inlined.
A **fully static** style is normalized at build time and emitted as a plain object with no runtime call at
all:
```tsx
// in
// out (style normalized at build time)
<_NativeText style={{ color: 'red' }} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()} />;
```
A **dynamic** style is routed through the `processTextStyle` runtime helper instead, where a reference
cache and a single `StyleSheet.flatten` are the win (more on that below):
```tsx
// in
// out
<_NativeText {..._processTextStyle(dynamicStyle)} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()} />;
```
Accessibility and `aria-*` props (and `disabled`) are collected into a single `processAccessibilityProps`
call; a negative `numberOfLines` literal is rewritten to `0`; an `id` is renamed to `nativeID`; a static
`userSelect` is lifted out of the style into a top-level `selectable` prop.
### View [#view-1]
`View` optimizes even with a `style` prop, since the wrapper passes style through unchanged:
```tsx
→ <_NativeView />
→ <_NativeView style={{ width: 1 }} />
→ <_NativeView nativeID="x" />
→ <_NativeView focusable={true} />
→ <_NativeView accessibilityLiveRegion="none" />
```
Static `aria-*`/`tabIndex`/`id` props are translated into their native counterparts **at build time**.
Dynamic values, or `aria-*` state/value groups that the wrapper merges, are routed through the
`processViewAccessibilityProps` helper:
```tsx
// in
// out
<_NativeView {..._processViewAccessibilityProps(Object.assign({}, { 'aria-label': label }))} />;
```
### Image [#image]
`Image` source, style, and accessibility props are normalized before Boost swaps the wrapper for
`NativeImage`. Static values are processed at build time. Dynamic values use small runtime helpers.
Compared to `Text` and `View`, the host component for `Image` is not imported directly. React Native does not export it through a supported interface, and only exports it through deprecated deep imports which log warnings when used. The plugin therefore registers the `RCTImageView` host directly.
For the complete matrix of what's optimized, translated, and skipped, see
[Coverage & Bailouts](/docs/information/optimization-coverage).
## Inside the plugin [#inside-the-plugin]
The plugin is a single Babel visitor on `JSXOpeningElement`. For each element it runs the `Text`,
`View`, and `Image` optimizers; each follows the same shape.
### Proving the element is really `react-native`'s [#proving-the-element-is-really-react-natives]
Two gates run first, and neither can be overridden:
* **`isReactNativeComponent`** checks the JSX binding, imported name, and `'react-native'` source. Aliases such as `import { Text as RNText } from 'react-native'` work, while local components and deep imports do not qualify.
### The bailout checks [#the-bailout-checks]
Each optimizer defines a list of **bailout checks**. If any fires (and the line isn't marked
`@boost-force`), the element is left untouched and logged as `skipped`. The checks encode "for this prop
or shape, the wrapper does something the host can't, or that Boost can't prove equivalent". This could be blacklisted
props, an unresolvable spread, non-primitive `Text` children, an unsafe ancestor, and so on.
A subtle one worth highlighting: `Text` children must be **provably primitive** (a string or number). A
non-string child could smuggle in a nested element, including another ``, which would break the
`TextAncestorContext` invariant. So `{name}` is optimized only when `name` provably resolves
to a string/number; `{maybeJSX()}` is not.
### Ancestor classification [#ancestor-classification]
The most intricate check is shared by all three optimizers. Components under a `Text` can need different
host semantics. A nested `Text` uses `RCTVirtualText`, and Android uses a separate inline Image host.
Before optimizing, the plugin therefore walks **up** the tree and classifies the ancestor chain as one of:
* `safe` — no `Text` ancestor anywhere up the chain → optimize.
* `text` — a `react-native` `Text` is an ancestor → skip.
* `unknown` — an ancestor is a component the plugin can't resolve → skip, unless you opt in.
The walk is more than a parent scan. It resolves JSX member expressions (`RN.View`), follows aliased
identifiers, recurses into **local function components** (including `memo()`/`forwardRef()` wrappers and
`props.children` render paths), and uses `WeakSet`s to break cycles in mutually-recursive components. When
it can't prove safety, it returns `unknown` and bails. False-positives (a missed optimization) are, as everywhere in the plugin, preferred over
false-negatives (a regression).
The `unknown` case is *often* safe in practice (third-party components rarely wrap children in `Text`), but there are still cases where optimizing components with an `unknown` ancestor could genuinely cause regressions. Therefore, Boost provides
explicit opt-in escape hatches: `dangerouslyOptimizeViewWithUnknownAncestors`,
`dangerouslyOptimizeTextWithUnknownAncestors` and `dangerouslyOptimizeImageWithUnknownAncestors` (see [Configuration](/docs/configuration/configure)).
### Rewriting and import injection [#rewriting-and-import-injection]
Once an element passes, the optimizer rewrites its props (the build-time translations above), then swaps
the JSX type and injects the needed import. Imports are **cached per file** on the Babel file object, so
even though the visitor fires thousands of times, each runtime symbol is imported exactly once.
## The runtime helpers [#the-runtime-helpers]
`react-native-boost/runtime` is the small library the generated code calls into. For reference, its full
API lives on the [Runtime Library](/docs/runtime-library) page. The most load-bearing pieces are:
* **`NativeText` / `NativeView`** resolve `unstable_NativeText` / `unstable_NativeView` from `react-native` at module load, and **gracefully fall back** to the standard `Text`/`View` on web or any runtime where these exports are missing.
* **`NativeImage`** loads React Native's public `Image` module to register its host, then renders the registered `RCTImageView` name directly. Web uses the standard `Image` component.
* **`processTextStyle(style)`** does the same flatten-and-normalize work as the wrapper, with one small difference: it **caches by reference in a `WeakMap`**. When you pass a `StyleSheet.create` reference, the first call flattens it and every later call returns the cached result. The wrapper re-flattens on every render. (Only stable references hit the cache; an inline `style={{…}}` is a fresh reference each render, so it re-flattens either way.)
* **`processAccessibilityProps(props)`** mirrors `Text`'s `aria-*` translation, `accessibilityState` merge, `disabled` reconciliation, and platform `accessible` default. It runs only when the element actually has accessibility props.
* **`processViewAccessibilityProps(props)`** does the same for `View`'s ARIA cluster (`aria-labelledby` split, live-region mapping, state/value aggregation, `tabIndex` → `focusable`).
## Why it's safe [#why-its-safe]
Boost is built on one strict contract: **never change rendered output, layout, or the accessibility tree.** Skipping a `Text` that could have been optimized just leaves
performance on the table; optimizing one that needed the wrapper would be a correctness bug with potential UI or behavior regressions.
Two escape hatches let you override the analysis when *you* know more than the plugin can prove:
[`@boost-force`](/docs/configuration/boost-decorator#boost-force) on a single element, or the
`dangerouslyOptimize*WithUnknownAncestors` options for whole-project ancestor resolution. Both are named
to signal that you've taken ownership of the correctness argument.
## React Native's own roadmap [#react-natives-own-roadmap]
The wrappers are not meant to live forever. React Native's core team has clearly stated the goal is to reduce the JS overhead as much as possible, making direct use of the native host components unnecessary:
The first example for this is the
`reduceDefaultPropsInText` feature flag: introduced in RN 0.82, it made the
`Text` wrapper assign derived accessibility/`aria-*` props **only when defined** (and spread the rest)
instead of always passing a fixed set of named props, so that prop keys whose value was `undefined` no longer
crossed the JS→native boundary. The feature flag **graduated** to default behavior in RN 0.85.
It's a real improvement. However, the wrapper continues to destructure every
prop, flatten styles, subscribe to context, and run as its own fiber on every render.
So, **React Native core is optimizing the runtime, while Boost patches the call site at build-time.** At some point in the future, in an ideal scenario, the JS wrapper components could become performant enough to make Boost genuinely unnecessary. However, today, it still offers a genuine performance improvement for a lot of apps.
# How It Works
In React Native, `Text`, `View`, and `Image` aren't quite what they appear to be. They look like basic building blocks, but each one is a small JavaScript component that runs **every time it renders**, wrapping a lower-level native component underneath.
That wrapper is genuinely useful. It handles edge cases and powers conveniences such as accessibility props, style normalization, and Image source processing. But many elements do not need that runtime work, so the wrapper becomes pure overhead. On a busy screen with hundreds of these components, that overhead adds up and starts costing you frames.
React Native Boost removes the wrapper when it isn't needed.
## The one-sentence version [#the-one-sentence-version]
At build time, Boost rewrites `Text`, `View`, and `Image` elements into the native components they were going to render anyway. The work the wrapper used to repeat on every render is either gone completely, or moved from the user's device to build-time.
## A quick before and after [#a-quick-before-and-after]
```tsx
// You write:
Hello
// Boost compiles it to (simplified):
Hello
```
`NativeText` is exactly what the `Text` wrapper would have rendered after doing the wrapper work outlines above. Boost just skips the middleman. The defaults the wrapper would have applied are baked in at build time, so what ends up on screen is identical.
These optimized components are imported from `react-native-boost/runtime` rather than `react-native` directly. That indirection lets them fall back to the standard components on web and other platforms where the native versions aren't available.
## Only when it's safe [#only-when-its-safe]
Boost never changes how your app looks or behaves. It rewrites a component only when it can **prove** that doing so is safe. For each supported component it checks a lot of things. For example:
* Is this really the `react-native` component, or some other `Text` from another library?
* Are its props fully compatible with the underlying native component?
* Is it in a safe spot in the tree (for example, not nested inside another `Text`)?
If any check fails, Boost leaves that component exactly as it was. The bias is strongly toward safety: it would rather miss an optimization than risk a bug.
# Optimization Coverage
React Native Boost is conservative by design. If it cannot prove an optimization is safe, it skips it. While this means it'll often skip optimizations that would be safe in practice, it also means that you can trust that optimizations that do happen are safe and won't cause behavioral changes or other bugs.
## At a Glance [#at-a-glance]
| Component | Optimized when... | Common bailout reasons |
| --------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Text` | Imported from `react-native`, no blacklisted props, primitive children, safe ancestor chain | `contains blacklisted props`, `has Text ancestor`, `has unresolved ancestor and dangerous optimization is disabled`, `has unresolved runtime parent that may render Text`, `contains non-primitive children`, `is a direct child of expo-router Link with asChild` |
| `View` | Imported from `react-native`, safe ancestor chain, no spread that may carry a translated prop | `has a spread that may carry a translated prop`, `has both a dynamic id and a nativeID (ambiguous precedence)`, `has Text ancestor`, `has unresolved ancestor and dangerous optimization is disabled` |
| `Image` | Imported from `react-native`, native platform known, supported source/style props, safe ancestor chain | `target platform is unknown`, `has a Unistyles style and there is no lean Image host to route to`, `has an unresolved style source that may be a Unistyles style`, `contains unsupported Image props`, `has a spread that may carry Image wrapper props`, `has Text ancestor`, `has unresolved ancestor and dangerous optimization is disabled` |
## Global Bailouts [#global-bailouts]
These skip optimization before component-specific checks:
* File path matches `ignores`
* Line is marked with `@boost-ignore`
Files skipped via `ignores` are filtered before optimizer checks, so you will not see per-component skip logs for
those files.
## Overriding Bailouts [#overriding-bailouts]
Use `@boost-force` to force optimization on a component that would otherwise be skipped. This bypasses all bailout checks except the `react-native` import check. See the [Decorators](/docs/configuration/boost-decorator#boost-force) page for details.
## Text Coverage [#text-coverage]
`Text` is optimized when all checks pass.
### Text blacklisted props [#text-blacklisted-props]
If any of these are present, the `Text` node is skipped:
* Interaction/responder props (`onPress`, `onLongPress`, `onResponder*`, `pressRetentionOffset`, `suppressHighlighting`, etc.)
* `selectionColor`
* `aria-hidden`
`id` is renamed to `nativeID` at build time (`id` wins when both are present). `Text` bails only if `id`/`nativeID` arrive via a spread, or a dynamic `id` appears alongside a `nativeID`.
### Text structure checks [#text-structure-checks]
* Children must be provably primitive (resolve to a `string` or `number`). Anything that could be a React element (nested elements, function calls, unresolved identifiers) bails as `contains non-primitive children`.
### Text ancestor safety checks [#text-ancestor-safety-checks]
Like `View`, `Text` optimization depends on ancestor classification:
* `safe`: optimize
* `text`: skip (`has Text ancestor`) — a `Text` nested in another `Text` renders as the inline `NativeVirtualText` host (`RCTVirtualText`), not `NativeText` (`RCTText`), so optimizing it would emit the wrong host.
* `unknown`: skip by default
A root `Text` returned by a component or renderer also skips as `has unresolved runtime parent that may render Text`. Its caller can mount it inside another `Text`, which requires `NativeVirtualText` instead of `NativeText`.
Set `dangerouslyOptimizeTextWithUnknownAncestors: true` to optimize unknown ancestors and runtime parents.
Enabling dangerous mode can introduce regressions if an unresolved ancestor or runtime parent renders a `Text`
wrapper. For example, `expo-router`'s `Link` wraps its children in a `` by default. A `Text` under a `Link`
must render as `NativeVirtualText`, and optimizing it would be wrong.
A `Text` used as a direct child of `expo-router` `Link` with `asChild` is **always** skipped
(`is a direct child of expo-router Link with asChild`), even under the dangerous flag, because `Link` makes that child
pressable.
```tsx
import { Link } from 'expo-router';
import { Text } from 'react-native';
Open profile
;
```
## View Coverage [#view-coverage]
`View` and `Text` share the same ancestor safety checks. The `View` wrapper translates a few ergonomic props into
native equivalents; Boost reproduces each translation at build time (or via a small runtime helper for dynamic values),
so these props no longer force a bailout.
### View translated props [#view-translated-props]
* `aria-*` → `accessibility*` (`aria-label`, `aria-labelledby`, `aria-live`, `aria-hidden`, and the state/value groups)
* `tabIndex` → `focusable`
* `id` → `nativeID` (`id` wins when both are present)
Everything else — including `accessible`, `accessibilityLabel`, and a lone `accessibilityState`/`accessibilityValue` —
is passed through unchanged.
### View prop bailouts [#view-prop-bailouts]
* A spread that may carry one of the translated props, since Boost cannot reach inside it to translate.
* A dynamic `id` alongside a `nativeID` (their runtime precedence cannot be resolved statically).
### Ancestor safety checks [#ancestor-safety-checks]
`View` optimization depends on ancestor classification:
* `safe`: optimize
* `text`: skip (`has Text ancestor`)
* `unknown`: skip by default
Set `dangerouslyOptimizeViewWithUnknownAncestors: true` to optimize `unknown` ancestors too.
Enabling dangerous mode can increase optimization coverage, but it can also introduce regressions if unresolved
ancestors render Text wrappers.
## Image Coverage [#image-coverage]
The `Image` optimizer rewrites supported `Image` elements when the target platform is known (`ios` or `android`) and the
source/style/accessibility props can be reproduced safely.
In Unistyles mode, an Image is skipped when its `style` is (or may be) a Unistyles style.
## Spread Props: Resolvable vs Unresolvable [#spread-props-resolvable-vs-unresolvable]
Unresolvable spread props are treated as unsafe and cause bailouts.
```tsx
// Usually optimizable (resolvable object literal)
Hello
// Usually skipped (cannot be statically resolved)
Hello
```
Same rule applies to `View`.
# Troubleshooting
## Quick Diagnostic Flow [#quick-diagnostic-flow]
1. Set `verbose: true` and `silent: false` in plugin config.
2. Restart Metro with cache clear.
3. Check skip reasons in logs.
4. Compare with the coverage rules in [Optimization Coverage](/docs/information/optimization-coverage).
## Common Issues [#common-issues]
### No optimization logs at all [#no-optimization-logs-at-all]
Likely causes:
* Plugin not loaded in `babel.config.js`
* `silent: true`
* File matched by `ignores`
Quick checks:
```js
module.exports = {
plugins: [
[
'react-native-boost/plugin',
{
verbose: true,
silent: false,
},
],
],
};
```
```bash
npm start -- --clear
```
### Skip reason: `contains blacklisted props` [#skip-reason-contains-blacklisted-props]
This is expected for unsupported prop sets, e.g. a `Text` with press/responder props, `selectionColor`, or
`aria-hidden`. (`View` translates accessibility props, `tabIndex`, and `id` instead of skipping; see
[Optimization Coverage](/docs/information/optimization-coverage#view-coverage).)
Fix options:
* Keep component as-is (recommended when semantics matter)
* Move unsupported behavior to a different node when possible
* Use `@boost-ignore` for explicit clarity
### Skip reason: `has a spread that may carry a translated prop` [#skip-reason-has-a-spread-that-may-carry-a-translated-prop]
A `View` with a spread (`{...props}`) Boost can't statically resolve, or that may contain a prop the `View` wrapper
translates (`aria-*`, `tabIndex`, `id`).
### Skip reason: `has unresolved ancestor and dangerous optimization is disabled` [#skip-reason-has-unresolved-ancestor-and-dangerous-optimization-is-disabled]
A `View` or `Text` is inside an ancestor React Native Boost cannot statically classify, so it cannot prove the
ancestor is not a `Text` (which would change the correct native host). A common case is a `Text` inside a
third-party wrapper such as `expo-router`'s `Link` (which itself wraps its children in a ``).
Options:
* Keep default behavior (safest)
* Use `@boost-force` on a specific line you have verified is safe
* Refactor ancestor/component structure to be statically obvious
* Enable `dangerouslyOptimizeViewWithUnknownAncestors` / `dangerouslyOptimizeTextWithUnknownAncestors` to override this behavior
### Skip reason: `has unresolved runtime parent that may render Text` [#skip-reason-has-unresolved-runtime-parent-that-may-render-text]
A root `Text` returned by a component or renderer can be mounted inside another `Text`. Boost keeps the wrapper because only that wrapper can select `NativeText` or `NativeVirtualText` from the runtime context.
Use `@boost-force` only when every use of that component has a non-text parent. The `dangerouslyOptimizeTextWithUnknownAncestors` option also overrides this check.
### Ignores do not work as expected in monorepos [#ignores-do-not-work-as-expected-in-monorepos]
`ignores` are resolved from Babel's working directory.
In nested apps, you may need explicit parent paths:
```js
ignores: ['../../node_modules/**'];
```
### Runtime import errors in app code [#runtime-import-errors-in-app-code]
The plugin injects imports from `react-native-boost/runtime`.
If you installed `react-native-boost` as a dev dependency, runtime imports can fail in app builds.
Fix: install it as a regular dependency.
# Runtime Library
`react-native-boost/runtime` is used by the Babel plugin to apply optimizations safely across platforms.
Besides re-exporting optimized native components with web-safe fallbacks, it also exposes helper utilities.
Direct usage is supported but generally not recommended unless needed for advanced integrations (for example,
[Nativewind setup](/docs/compatibility/nativewind)).
## API Reference [#api-reference]
This section is automatically generated from runtime exports.