Skip to content

Use asset catalog for iOS images#30129

Open
janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:assets-catalog
Open

Use asset catalog for iOS images#30129
janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:assets-catalog

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Oct 7, 2020

Copy link
Copy Markdown
Contributor

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. The jpeg routing through the bundle-asset loaders is itself gated on the key, so apps that have not opted in keep today's behavior exactly.
  • 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 note: whether App Store slicing thins an Assets.car inside a nested bundle is not publicly documented and still needs verification with a thinned export. Worst case it does not, which matches today's behavior — loose packager images are never thinned either (all scales ship to every device) — so this change is never worse than the status quo on download size, and strictly better on load time.

Migration

To opt an app in, add to its Info.plist:

<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.
  • Assets that only exist at a non-standard scale (e.g. only @1.5x) currently get dropped by actool with a warning in the build log — a community-cli-plugin follow-up will route those to plain files instead.
  • 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.

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: the script emits the Catalyst-specific actool invocation (--platform macosx --ui-framework-family uikit), which was verified to compile the catalog correctly, and the runtime uses the same imageNamed:inBundle: mechanism CocoaPods resource bundles use on Catalyst — but no end-to-end Catalyst build was run (rn-tester does not enable Catalyst).

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Contributor A React Native contributor. labels Oct 7, 2020
@pull-bot

pull-bot commented Oct 7, 2020

Copy link
Copy Markdown
Warnings
⚠️ 🔒 package.json - Changes were made to package.json. This will require a manual import by a Facebook employee.

Generated by 🚫 dangerJS against 1c7e31ba2ab80466cf837700b1c3d91f382a0412

@react-native-bot react-native-bot added Platform: iOS iOS applications. Type: Enhancement A new feature or enhancement of an existing feature. labels Oct 7, 2020
@analysis-bot

analysis-bot commented Oct 7, 2020

Copy link
Copy Markdown
Platform Engine Arch Size (bytes) Diff
ios - universal n/a --

Base commit: 950ea91
Branch: main

@analysis-bot

analysis-bot commented Oct 7, 2020

Copy link
Copy Markdown
Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 8,895,039 +1
android hermes armeabi-v7a 7,943,587 +2
android hermes x86 9,292,924 +1
android hermes x86_64 9,194,442 -2
android jsc arm64-v8a 9,482,023 +0
android jsc armeabi-v7a 8,423,567 +0
android jsc x86 9,466,043 -1
android jsc x86_64 9,780,275 +2

Base commit: c27e9e6
Branch: main

@thymikee

Copy link
Copy Markdown
Contributor

@janicduplessis I haven't dived deep into assets stuff on the CLI side and here, but maybe you have a better overview on this. Now that assets logic is scoped under @react-native/assets package, maybe we could reuse it on the CLI side? We should strive to reuse everything that's possible to avoid these weird syncing situations (this is not the first time).

@janicduplessis

Copy link
Copy Markdown
Contributor Author

@thymikee This already uses the shared code in the assets package and updates it (just rename a method since it is now used on iOS too).

@janicduplessis

Copy link
Copy Markdown
Contributor Author

@sota000 Any interest on landing this? I've been using this in my fork for a long time and is stable.

@grabbou

grabbou commented Feb 15, 2022

Copy link
Copy Markdown
Contributor

It would be great to have a status on this one, there's an outstanding PR in the CLI too.

@kelset

kelset commented Feb 15, 2022

Copy link
Copy Markdown
Contributor

One think I'd guess would be fixing the conflicts, but yeah also would be great to get some internal 👍/👎 if it's even worth

@janicduplessis janicduplessis marked this pull request as ready for review October 3, 2022 23:47
@janicduplessis janicduplessis requested a review from hramos as a code owner October 3, 2022 23:47
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 4, 2022
@cortinico

Copy link
Copy Markdown
Contributor

@janicduplessis can we rebase + fix the conflicts here?

@janicduplessis janicduplessis force-pushed the assets-catalog branch 2 times, most recently from 050a242 to f527b5e Compare October 4, 2022 18:49
@janicduplessis

Copy link
Copy Markdown
Contributor Author

Ok I rebased this and tested again in RN tester. I added instructions on how to do so in the Test Plan. It does require the CLI patched with react-native-community/cli#1290 to work.

I think the main part that is tricky with this is if the app template is not properly updated or for some reason an older version of the cli is used it will break. For the old cli version case it is actually not that bad since the build will error because of invalid parameter --asset-catalog-dest. If the template project is not properly configured then assets won't show in release mode only.

I think it is still fine to move forward, but might want to make sure to document the change to make sure templates are updated properly. The main 2 things that need updating is to create the RNAssets.xcassets catalog and to reorder build scripts so that JS bundle is built before Copy Bundle Resources. For new projects and automated updates that should be fine, but might not be obvious for someone updating manually.

@cipolleschi

Copy link
Copy Markdown
Contributor

/rebase

@facebook-github-bot

Copy link
Copy Markdown
Contributor

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@cipolleschi

Copy link
Copy Markdown
Contributor

Weird, this PR makes the template tests fail... Basically, the CLI is not able to create a RN app... Is it expected because it requires react-native-community/cli#1290 ?

@janicduplessis

Copy link
Copy Markdown
Contributor Author

Oh I see the problem, the template yarn install fails because of error Couldn't find any versions for "@react-native/assets" that matches "1.1.0". Not sure what would be best to handle updating the @react-native/assets package. It is basically just renaming a function since we use it on iOS too now.

@janicduplessis

Copy link
Copy Markdown
Contributor Author

We might also have build issue later for ios release build since it does require the cli patch

@cortinico cortinico left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a packages/react-native-codegen/react-native-codegen-0.0.5.tgz that needs to go 👍

Comment thread packages/assets/package.json Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would require a npm publish of this package OR the use of Verdaccio (which we already have for template tests).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might also conflict with #34572. Maybe not all template tests use verdaccio currently? Do I need to make some changes for it to pickup the assets package?

Maybe the simplest way would be to just revert the change to the assets package and can land it later as it is not really needed as all it does is rename a function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the simplest way would be to just revert the change to the assets package and can land it later as it is not really needed as all it does is rename a function.

If possible, let's revert it. We're in the process of migrating our repo to be a proper monorepo and let's try to limit the changes to those packages to only the necessary ones 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I reverted the change, let's see how CI goes.

@janicduplessis

Copy link
Copy Markdown
Contributor Author

@cortinico @cipolleschi Template tests are all passing except the ios release ones, with error error: unknown option '--asset-catalog-dest', which is expected since it doesn’t include the updated cli. Maybe the next step would be to get the cli PR merged and published.

Comment thread React/Base/RCTUtils.m Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a regular expression, could you use a NSCharacterSet for this?

Comment thread React/Base/RCTUtils.m Outdated

@javache javache Nov 10, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a link to documentation we could provide here?

I'm worried that this warns and then just continues executing, so the warning will likely be ignored.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could provide a link to the release notes where it explains this change. However this doesn’t exists yet so I guess it could be added later?

@cipolleschi

Copy link
Copy Markdown
Contributor

@janicduplessis, there are a couple of comments by @javache that should be addressed. Could you take care of it, please? 🙏

@janicduplessis

Copy link
Copy Markdown
Contributor Author

Yes, haven’t had the time to address those yet. I should be able to in the next week or so.

@cipolleschi

Copy link
Copy Markdown
Contributor

/rebase

1 similar comment
@cipolleschi

Copy link
Copy Markdown
Contributor

/rebase

@facebook-github-bot

Copy link
Copy Markdown
Contributor

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

facebook-github-bot pushed a commit that referenced this pull request Dec 12, 2023
Summary:
There is currently an error when building in release on iOS when using asset catalogs (experimental feature that is partially merged #30129)

This was probably incorrectly migrated from the community cli repo. `.imageset` is actually folders so it needs to be removed with `{recursive: true, force: true}`. I also renamed the variable `files` which is confusing since its folders.

## Changelog:

[IOS] [FIXED] - Fix cleanAssetCatalog error

Pull Request resolved: #41865

Test Plan: Tested in an app that uses asset catalogs

Reviewed By: NickGerleman

Differential Revision: D52032258

Pulled By: huntie

fbshipit-source-id: 1dc0ca09e0da0d514b03d7d72707bdcaef03301d
@github-actions

github-actions Bot commented Feb 1, 2024

Copy link
Copy Markdown

This PR is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions Bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Feb 1, 2024
@janicduplessis

Copy link
Copy Markdown
Contributor Author

I will one day find time to finish this :)

@github-actions github-actions Bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Feb 4, 2024
@cortinico cortinico added the Never gets stale Prevent those issues and PRs from getting stale label Feb 4, 2024
@janicduplessis janicduplessis force-pushed the assets-catalog branch 2 times, most recently from dce8343 to 819db88 Compare July 7, 2026 04:31
@janicduplessis janicduplessis changed the title Use asset catalog for ios images Use asset catalog for iOS images Jul 7, 2026
@janicduplessis janicduplessis force-pushed the assets-catalog branch 9 times, most recently from d554ce3 to fc7fed7 Compare July 8, 2026 01:51
Load packager image assets from a compiled asset catalog instead of loose
files in the app bundle. At build time react-native-xcode.sh has the CLI emit
imagesets into a staging 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 with [UIImage imageNamed:inBundle:].

The feature is opt-in via the RCTUseAssetCatalog Info.plist key, which 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 build and runtime cannot disagree on where
image assets live. The script owns the catalog end to end, so migrating an
app is adding the one Info.plist key: no .xcassets to create and no Xcode
project changes. The app's own asset pipeline (Images.xcassets, asset symbol
generation, CompileAssetCatalog) is untouched, and apps that have not
migrated are unaffected.

The catalog path is a single lookup with no filesystem fallback; a mis-bundled
asset logs an RCTLogError instead of failing silently. The native side only
resolves catalog names for what the CLI actually emits into the catalog
(png/jpg/jpeg under main-bundle assets/, mirroring isCatalogAsset), so
gif/webp packager assets, sub-bundles and OTA assets outside the main bundle
fall through to the existing loader. jpeg is also added to
RCTIsImageAssetsPath so jpeg assets are routed to the bundle-asset loaders.

rn-tester and private/helloworld opt in via their Info.plists. helloworld's
bundle phase now also substitutes REACT_NATIVE_PATH in CONFIG_JSON, which any
bundling build requires.

Cold image loads in RNTester are ~15x faster (median 47us vs 719us).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Contributor A React Native contributor. Needs: React Native Team Attention Never gets stale Prevent those issues and PRs from getting stale Platform: iOS iOS applications. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. Type: Enhancement A new feature or enhancement of an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.