You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Summary:
Use an asset catalog for images on iOS. At build time, `react-native-xcode.sh` has the bundler emit packager image assets into a staging asset catalog (`--asset-catalog-dest`, already in `react-native/community-cli-plugin`), compiles it with `actool` into an `RNAssets.bundle` inside the app, and the native image loader resolves images by name from that bundle's `Assets.car` with `[UIImage imageNamed:inBundle:]` instead of reading loose files.
Properties worth calling out:
- **One opt-in switch, no project changes.** The feature is gated on the `RCTUseAssetCatalog` key in the app's Info.plist, and that key is the single source of truth: the native loader reads it (a build-time constant, read once), and `react-native-xcode.sh` reads the same key to decide whether to bundle images into the catalog — so the build and the runtime cannot disagree on where image assets live. Because the script owns the catalog end to end (staged in derived files, compiled into the app's resources next to `main.jsbundle`), **migration is adding one Info.plist key** — no `.xcassets` to create, no build-phase or project changes.
- **No fallback.** The catalog path is a single lookup with no filesystem fallback — a mis-bundled asset logs an `RCTLogError` (instead of silently rendering nothing) rather than adding an `fs` stat to the hot path.
- **Parity with the CLI, OTA-safe.** The native side resolves a catalog name only for what the CLI actually emits into the catalog: png/jpg/jpeg under main-bundle `assets/…` (mirroring `isCatalogAsset`). Everything else — gif/webp packager assets, `.bundle` sub-bundles, OTA assets outside the main bundle — falls through to the existing loader. (This also adds `jpeg` to `RCTIsImageAssetsPath`, which previously listed only `png`/`jpg` — an oversight, since `.jpeg` is the same kind of bundled image; it is now treated like `png`/`jpg` regardless of the catalog.)
- **No interference with Xcode's own asset pipeline.** The app's `Images.xcassets`, asset symbol generation (`UIImage(resource:)`), and `CompileAssetCatalog` are untouched: the RN catalog never enters the Xcode project, so there are no build-phase ordering constraints and nothing to disable.
## Changelog:
[iOS] [Added] - Use asset catalog for ios images
## Performance
**What was measured:** the time to *resolve* an asset to a `UIImage` inside `RCTImageFromLocalAssetURL` — a compiled-catalog name lookup vs. the legacy `imageNamed:` search + `imageWithContentsOfFile:` on loose files. Decode is deferred in both paths (and costs the same), so this is resolve latency, not end-to-end render time. It matters because `RCTLocalAssetImageLoader` loads local assets synchronously to avoid flicker, so this time sits on the critical path once per distinct image. Setup: RNTester, Release, new arch, iOS simulator, first load of each distinct image (UIKit caches repeats — repeat loads are ~10 µs in both builds).
Raw per-load numbers (final implementation, `RNAssets.bundle`), in load order:
| # | image | catalog (µs) | filesystem (µs) |
|---|---|---:|---:|
| 1 | `searchicon` ¹ | 1,081 | 4,497 |
| 2 | `bottomnavcomponentsicondark` | 320 | 710 |
| 3 | `bottomnavplaygroundsiconlight` | 147 | 422 |
| 4 | `bottomnavapisiconlight` | 29 | 352 |
| 5 | `bottomnavcomponentsiconlight` | 320 | 1,410 |
| 6 | `uie_thumb_normal` | 67 | 667 |
| 7 | `uie_thumb_selected` | 32 | 468 |
| 8 | `uie_comment_normal` | 26 | 454 |
| 9 | `uie_comment_highlighted` | 31 | 671 |
| 10 | `verylargeimage` ² | 30 | 1,855 |
| 11 | `alphahotdog` | 171 | 592 |
¹ First load in each build pays one-time costs: mapping `Assets.car` on the catalog side, ImageIO/framework warm-up + first disk touch on the filesystem side.
² Resolve only — the large image's decode cost is unchanged, so its end-to-end win is much smaller than this row suggests.
Every image is faster from the catalog: median 67 µs vs 667 µs (**10×**; an earlier run of the same benchmark measured 47 µs vs 719 µs, ~15× — run-to-run variance, same order either way). The mechanism: the catalog is a single memory-mapped, indexed archive (one name lookup, no per-image syscalls), while the loose-file path does an `imageNamed:` search over several filename permutations plus a per-image file open.
The URL→catalog-name derivation on the native side is a single character pass with no regex; benchmarked against a straightforward regex-based implementation of the same transform it measures 4.9 µs vs 11.0 µs per call (2.2×, Debug build, including the shared URL→bundle-path resolution both share), so the name mapping is a negligible part of the lookup.
**App thinning:** verified that App Store slicing thins the `Assets.car` inside `RNAssets.bundle` exactly like the app's own catalog. Test: a fixture app with a 1x/2x/3x imageset in both its main `Images.xcassets` and an actool-compiled `RNAssets.bundle`, archived and exported with `thinning` for a 3x device — both cars were sliced to 3x-only. So unused scales are stripped per device (unlike today's loose packager files, which ship every scale to every device).
## Migration
To opt an app in, add to its `Info.plist`:
```xml
<key>RCTUseAssetCatalog</key>
<true/>
```
That's the entire migration — no `.xcassets` to create, no build-phase or project changes. Notes:
- **Do a clean build after changing the key.** Incremental builds don't remove image assets a previous build placed with the other setting (unused, but dead weight in local builds; archives are unaffected).
- The key must be a literal value in the app's source Info.plist: the build script reads it with PlistBuddy (accepting the same value forms the runtime `boolValue` check accepts), so build-setting substitution or fully generated Info.plists (`GENERATE_INFOPLIST_FILE`) aren't seen by the script and stay on the legacy path. If bundling happens outside `react-native-xcode.sh` (custom CI calling `react-native bundle` directly) while the key is enabled, the native side detects the missing `RNAssets.bundle` and logs an actionable error instead of silently rendering nothing.
- Incomplete but valid scale sets degrade gracefully: an imageset with, say, only a `2x` (no `3x`) is kept for a 3x device by App Store slicing and used at runtime — verified with a thinned export. Only a *non-integer*-only asset (e.g. an Android-density `1.5x` with no `1x`/`2x`/`3x` variant, which iOS asset catalogs cannot represent) is unsupported: `actool` drops it with an `Unknown scale value` warning in the build log and the runtime logs an error. That's a rare antipattern; a `community-cli-plugin` follow-up will turn it into a clear build-time error at the source (it has the scale and asset path), rather than this repo re-parsing actool output.
- OTA updates keep working: assets delivered outside the main bundle never resolve to the catalog and use the regular loader.
- Docs follow-up: a section on the website's Images page (`react-native-website`) and the one-line opt-in in `react-native-community/template` will be separate PRs, timed with the release that ships this.
Pull Request resolved: #30129
Test Plan:
RNTester and `private/helloworld` both opt in (`RCTUseAssetCatalog=YES` in their Info.plists — the only change an app needs), so CI covers the catalog path on both app shapes. The new-project template lives in `react-native-community/template` and will get the same one-line opt-in in a follow-up PR there. To test locally with RNTester:
1. In Xcode, edit the RNTester scheme → set Build Configuration to Release.
2. Build & run. Local images render, served from `RNAssets.bundle/Assets.car`; the app contains no loose packager png/jpg files.
Unit coverage in `RCTURLUtilsTests` (`testAssetCatalogNameForURL`), asserting the identifiers the CLI generates: scale-suffix stripping (including non-integer `1.5x`), folder encoding, illegal-char removal, jpeg vs. non-catalog types (gif), out-of-project-root assets, query strings, non-packager paths, and nil.
Mac Catalyst: verified end-to-end by building rn-tester as a Mac Catalyst app — the catalog compiles (via the Catalyst-specific `--platform macosx --ui-framework-family uikit` actool invocation) into `RNTester.app/Contents/Resources/RNAssets.bundle` with all packager images and no loose files, and the runtime uses the same `imageNamed:inBundle:` mechanism CocoaPods resource bundles use on Catalyst. (rn-tester doesn't ship a Catalyst config, so this isn't in CI, but the build is clean.)
Reviewed By: cipolleschi
Differential Revision: D40095766
Pulled By: javache
fbshipit-source-id: 18dfcb47ae5185ef279b20dd0087de9a31e930b4
Copy file name to clipboardExpand all lines: packages/rn-tester/RNTester/Info.plist
+2Lines changed: 2 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -48,6 +48,8 @@
48
48
<string>You need to add NSPhotoLibraryUsageDescription key in Info.plist to enable photo library usage, otherwise it is going to *fail silently*!</string>
0 commit comments