Skip to content

Commit 82dca04

Browse files
committed
feat(image): manually register host and promote optimizer to default
1 parent 9f9b298 commit 82dca04

15 files changed

Lines changed: 55 additions & 183 deletions

File tree

apps/docs/content/docs/configuration/configure.mdx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,14 @@ module.exports = {
2323
optimizations: {
2424
text: true,
2525
view: true,
26-
image: false,
26+
image: true,
2727
},
2828
},
2929
],
3030
],
3131
};
3232
```
3333

34-
`Image` optimization is currently **opt-in** because it depends on deprecated React Native import paths. Enabling it will log React Native import deprecation warnings to the console. To enable it, explicitly flip it to `true`:
35-
36-
```js
37-
['react-native-boost/plugin', { optimizations: { image: true } }];
38-
```
39-
4034
## Plugin Options
4135

4236
<AutoOptionSections path="../../packages/react-native-boost/src/plugin/types/index.ts" name="PluginOptions" idPrefix="plugin-options" />

apps/docs/content/docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
99

1010
React Native Boost consists of two pieces:
1111

12-
- A Babel plugin that statically analyzes your source code and replaces safe `Text` and `View` components with their direct native counterparts, leading to significant performance improvements compared to the JS-based wrapper components.
12+
- 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.
1313
- A runtime package used internally by the plugin for cross-platform-safe imports and helper utilities.
1414

1515
The analyzer is intentionally strict and skips any optimizations that may lead to user-facing bugs and behavioral changes.

apps/docs/content/docs/information/deep-dive.mdx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ Fewer fibers means less for React to build, diff, and commit on every frame (som
210210

211211
## What Boost emits
212212

213-
Boost's core move is to swap the JSX element's type from `Text`/`View` to `NativeText`/`NativeView`
214-
(imported from `react-native-boost/runtime`), after reproducing (at *build time*) whatever inescapable
215-
work the wrapper would have done for the specific element.
213+
Boost's core move is to swap the JSX element's type from `Text`/`View`/`Image` to
214+
`NativeText`/`NativeView`/`NativeImage` (imported from `react-native-boost/runtime`), after reproducing
215+
(at *build time*) whatever inescapable work the wrapper would have done for the specific element.
216216

217217
### Text
218218

@@ -284,21 +284,27 @@ Dynamic values, or `aria-*` state/value groups that the wrapper merges, are rout
284284
<_NativeView {..._processViewAccessibilityProps(Object.assign({}, { 'aria-label': label }))} />;
285285
```
286286

287+
### Image
288+
289+
`Image` source, style, and accessibility props are normalized before Boost swaps the wrapper for
290+
`NativeImage`. Static values are processed at build time. Dynamic values use small runtime helpers.
291+
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.
292+
287293
For the complete matrix of what's optimized, translated, and skipped, see
288294
[Coverage & Bailouts](/docs/information/optimization-coverage).
289295

290296
## Inside the plugin
291297

292-
The plugin is a single Babel visitor on `JSXOpeningElement`. For each element it runs the `Text`
293-
optimizer and the `View` optimizer; each follows the same shape.
298+
The plugin is a single Babel visitor on `JSXOpeningElement`. For each element it runs the `Text`,
299+
`View`, and `Image` optimizers; each follows the same shape.
294300

295301
<Mermaid
296302
chart={`flowchart TD
297-
el["JSXOpeningElement (Text or View)"] --> g{"Resolves to the react-native component?"}
303+
el["JSXOpeningElement (Text, View, or Image)"] --> g{"Resolves to the react-native component?"}
298304
g -- no --> skip["Leave unchanged"]
299305
g -- yes --> b{"Does any bailout check fail?"}
300306
b -- "yes, and no @boost-force" --> skip
301-
b -- "no, or @boost-force" --> rw["Rewrite props, swap type to NativeText / NativeView"]
307+
b -- "no, or @boost-force" --> rw["Rewrite props, swap type to its native host"]
302308
rw --> imp["Inject the runtime import (cached once per file)"]`}
303309
/>
304310

@@ -323,9 +329,9 @@ to a string/number; `<Text>{maybeJSX()}</Text>` is not.
323329

324330
### Ancestor classification
325331

326-
The most intricate check is shared by both optimizers. An element nested inside a `Text` must render as
327-
the *inline* host (`RCTVirtualText`), not the *block* host — so before optimizing, the plugin walks **up**
328-
the tree and classifies the ancestor chain as one of:
332+
The most intricate check is shared by all three optimizers. Components under a `Text` can need different
333+
host semantics. A nested `Text` uses `RCTVirtualText`, and Android uses a separate inline Image host.
334+
Before optimizing, the plugin therefore walks **up** the tree and classifies the ancestor chain as one of:
329335

330336
- `safe` — no `Text` ancestor anywhere up the chain → optimize.
331337
- `text` — a `react-native` `Text` is an ancestor → skip.
@@ -338,8 +344,8 @@ it can't prove safety, it returns `unknown` and bails. False-positives (a missed
338344
false-negatives (a regression).
339345

340346
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
341-
explicit opt-in escape hatches: `dangerouslyOptimizeViewWithUnknownAncestors` and
342-
`dangerouslyOptimizeTextWithUnknownAncestors` (see [Configuration](/docs/configuration/configure)).
347+
explicit opt-in escape hatches: `dangerouslyOptimizeViewWithUnknownAncestors`,
348+
`dangerouslyOptimizeTextWithUnknownAncestors` and `dangerouslyOptimizeImageWithUnknownAncestors` (see [Configuration](/docs/configuration/configure)).
343349

344350
### Rewriting and import injection
345351

@@ -353,6 +359,7 @@ even though the visitor fires thousands of times, each runtime symbol is importe
353359
API lives on the [Runtime Library](/docs/runtime-library) page. The most load-bearing pieces are:
354360

355361
- **`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.
362+
- **`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.
356363
- **`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.)
357364
- **`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.
358365
- **`processViewAccessibilityProps(props)`** does the same for `View`'s ARIA cluster (`aria-labelledby` split, live-region mapping, state/value aggregation, `tabIndex``focusable`).

apps/docs/content/docs/information/how-it-works.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ title: How It Works
33
description: Why React Native Boost makes your app faster (the short version).
44
---
55

6-
In React Native, `Text` and `View` 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.
6+
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.
77

8-
That wrapper is genuinely useful. It handles a lot of edge cases and powers conveniences like `aria-*` props, `userSelect`, and clamping `numberOfLines`. But most of the time your `Text` or `View` uses none of that, and the wrapper's work is pure overhead. On a busy screen with hundreds of these components, that overhead adds up and starts costing you frames.
8+
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.
99

1010
React Native Boost removes the wrapper when it isn't needed.
1111

1212
## The one-sentence version
1313

14-
At build time, Boost rewrites `Text` and `View` 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.
14+
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.
1515

1616
## A quick before and after
1717

@@ -29,7 +29,7 @@ These optimized components are imported from `react-native-boost/runtime` rather
2929

3030
## Only when it's safe
3131

32-
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 `Text` or `View` it checks a lot of things. For example:
32+
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:
3333

3434
- Is this really the `react-native` component, or some other `Text` from another library?
3535
- Are its props fully compatible with the underlying native component?

apps/docs/content/docs/information/optimization-coverage.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ React Native Boost is conservative by design. If it cannot prove an optimization
1111
| --- | --- | --- |
1212
| `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`, `contains non-primitive children`, `is a direct child of expo-router Link with asChild` |
1313
| `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` |
14-
| `Image` | Opted in with `optimizations.image: true`, 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` |
14+
| `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` |
1515

1616
## Global Bailouts
1717

@@ -113,9 +113,7 @@ Set `dangerouslyOptimizeViewWithUnknownAncestors: true` to optimize `unknown` an
113113

114114
## Image Coverage
115115

116-
`Image` optimization is opt-in for now because it uses deprecated React Native deep imports, which may print deprecation warnings. See [Configure the Babel Plugin](/docs/configuration/configure).
117-
118-
The optimizer rewrites supported `Image` elements when the target platform is known (`ios` or `android`) and the
116+
The `Image` optimizer rewrites supported `Image` elements when the target platform is known (`ios` or `android`) and the
119117
source/style/accessibility props can be reproduced safely.
120118

121119
In Unistyles mode, an Image is skipped when its `style` is (or may be) a Unistyles style.

packages/react-native-boost/src/plugin/__tests__/parity/boost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function transformBoostCase(os: PlatformOS, jsxBody: string, preamble = ''): str
3232
filename: 'boost-case.jsx',
3333
caller: { name: 'metro', platform: os } as TransformCaller,
3434
presets: [['@babel/preset-react', { runtime: 'automatic' }]],
35-
plugins: [[boostPlugin, { silent: true, optimizations: { image: true } }]],
35+
plugins: [[boostPlugin, { silent: true }]],
3636
});
3737
return out!.code!;
3838
}

packages/react-native-boost/src/plugin/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default declare((api, rawOptions, dirname?: string) => {
5050
if (isIgnoredFile(path, options.ignores ?? [])) return;
5151
if (options.optimizations?.text !== false) textOptimizer(path, logger, options, platform, unistylesEnabled);
5252
if (options.optimizations?.view !== false) viewOptimizer(path, logger, options, platform, unistylesEnabled);
53-
if (options.optimizations?.image === true) imageOptimizer(path, logger, options, platform, unistylesEnabled);
53+
if (options.optimizations?.image !== false) imageOptimizer(path, logger, options, platform, unistylesEnabled);
5454
},
5555
},
5656
};

packages/react-native-boost/src/plugin/optimizers/image/__tests__/index.test.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -132,35 +132,6 @@ pluginTester({
132132
],
133133
});
134134

135-
describe('image plugin option', () => {
136-
it('keeps Image optimization opt-in in the full plugin', async () => {
137-
const source = `
138-
import { Image } from 'react-native';
139-
<Image source={{ uri: 'logo.png', width: 16, height: 16 }} />;
140-
`;
141-
142-
const defaultOutput = await formatTestResult(
143-
transformSync(source, {
144-
configFile: false,
145-
babelrc: false,
146-
caller: { name: 'metro', platform: 'ios' } as TransformCaller,
147-
plugins: ['@babel/plugin-syntax-jsx', [boostPlugin, { silent: true }]],
148-
})!.code!
149-
);
150-
const enabledOutput = await formatTestResult(
151-
transformSync(source, {
152-
configFile: false,
153-
babelrc: false,
154-
caller: { name: 'metro', platform: 'ios' } as TransformCaller,
155-
plugins: ['@babel/plugin-syntax-jsx', [boostPlugin, { silent: true, optimizations: { image: true } }]],
156-
})!.code!
157-
);
158-
159-
expect(defaultOutput).not.toContain('NativeImage');
160-
expect(enabledOutput).toContain('NativeImage');
161-
});
162-
});
163-
164135
describe('image android output', () => {
165136
it('emits Android top-level empty headers for src sources', async () => {
166137
const output = await transformImage(

packages/react-native-boost/src/plugin/types/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ export interface PluginOptimizationOptions {
1313
view?: boolean;
1414
/**
1515
* Whether to optimize the `Image` component.
16-
*
17-
* Uses deprecated React Native deep imports and may print React Native deep-import deprecation
18-
* warnings, so it is opt-in for now.
19-
* @default false
16+
* @default true
2017
*/
2118
image?: boolean;
2219
}
@@ -48,7 +45,7 @@ export interface PluginOptions {
4845
/**
4946
* Toggle individual optimizers.
5047
*
51-
* If omitted, `Text` and `View` are enabled and `Image` stays disabled.
48+
* If omitted, all optimizers are enabled.
5249
*/
5350
optimizations?: PluginOptimizationOptions;
5451
/**

packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const generateCombinedTestPlugin = (options: PluginOptions = {}, platform
4949
JSXOpeningElement(path) {
5050
textOptimizer(path, logger, options, platform, unistylesEnabled);
5151
viewOptimizer(path, logger, options, platform, unistylesEnabled);
52-
if (options.optimizations?.image === true) imageOptimizer(path, logger, options, platform, unistylesEnabled);
52+
if (options.optimizations?.image !== false) imageOptimizer(path, logger, options, platform, unistylesEnabled);
5353
},
5454
},
5555
};

0 commit comments

Comments
 (0)