<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>App&amp;Flow Blog</title><description>Deep technical dives into solving complex React Native engineering challenges and building outstanding software by App&amp;Flow.</description><link>https://blog.appandflow.com</link><language>en</language><item><title>Introducing Stim: React Native environments for coding agents</title><link>https://blog.appandflow.com/introducing-stim-react-native-environments-for-coding-agents</link><guid isPermaLink="true">https://blog.appandflow.com/introducing-stim-react-native-environments-for-coding-agents</guid><description>Stim is an open-source CLI that gives coding agents isolated React Native environments, shared native build caches across worktrees, and feedback designed for unattended use.</description><pubDate>Thu, 10 Sep 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Coding agents can write great React Native code. Getting that code built and running still takes work: starting Metro, choosing a device, waiting for native builds, and figuring out whether the app actually launched.&lt;/p&gt;&lt;p&gt;Most of that tooling assumes a developer is there to answer prompts and manage the environment. Add multiple agents working in separate checkouts, and they can end up fighting over the same port or simulator, or compiling the same native app again.&lt;/p&gt;&lt;p&gt;At App &amp;amp; Flow, we built Stim to make that part easier. It’s an open-source CLI that gives coding agents isolated React Native environments, with shared build caches that work across worktrees and an interface designed for unattended use.&lt;/p&gt;&lt;h2 id=&quot;what-stim-handles&quot;&gt;What Stim handles&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Separate environments for parallel work.&lt;/strong&gt; Each workspace gets its own Metro port and simulator. Agents can work on different changes without connecting to another checkout’s dev server or taking over its device.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reuse instead of rebuilding.&lt;/strong&gt; When native inputs match, Stim can install an existing app build in a new worktree. It also shares compiler and Metro caches, so creating a separate checkout doesn’t mean starting all the expensive work over.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Useful feedback for the agent.&lt;/strong&gt; Non-interactive commands report the device, app, build, and launch state. Focused errors and logs help the agent find what went wrong without reading an entire build transcript.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cleanup when the task is done.&lt;/strong&gt; Stim tracks the resources it creates and provides commands to release them, without deleting your own simulators or emulators.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Stim works with React Native Community CLI and Expo projects on iOS and Android. Builds run locally, using your existing development setup. It doesn’t replace your coding agent or device automation tools; it handles the environment they need to build, launch, and inspect the app.&lt;/p&gt;&lt;p&gt;The simulator can also run remotely through &lt;a href=&quot;https://expo.dev/services/simulators&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;EAS Simulator&lt;/a&gt;, in early access. Stim builds the app locally, connects it to the remote environment, and manages the session’s lifecycle. This keeps the simulator off your machine while preserving the agent’s build-and-check workflow.&lt;/p&gt;&lt;h2 id=&quot;from-a-code-change-to-a-checked-result&quot;&gt;From a code change to a checked result&lt;/h2&gt;&lt;p&gt;Say you ask an agent to fix a screen in a separate worktree and show you that it works. The agent creates the checkout, then uses Stim to prepare its environment and launch the app on its own simulator. If a compatible native build already exists, Stim reuses it.&lt;/p&gt;&lt;p&gt;Once the app is running, the agent can use a device automation tool such as &lt;a href=&quot;https://agent-device.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;agent-device&lt;/a&gt; to navigate to the screen, check the change, and capture a screenshot or recording. The agent can then open a PR with that proof. Stim handles the build and launch; the agent still needs to verify the result. Another agent can work on a different task at the same time without sharing that environment.&lt;/p&gt;&lt;h2 id=&quot;what-we-saw-in-practice&quot;&gt;What we saw in practice&lt;/h2&gt;&lt;p&gt;We recorded the same JavaScript change task with GPT-5.6 Sol on one M4 Mac mini with 16 GB of memory: create a new worktree, make the change, launch the iOS app, and verify the updated screen.&lt;/p&gt;&lt;p&gt;The run using Stim reached screenshot proof in &lt;strong&gt;2m14s&lt;/strong&gt;, compared with &lt;strong&gt;10m11s&lt;/strong&gt; using local Expo and Apple tooling. Both started from a built main checkout and carried existing dependencies and native outputs into the new worktree. Stim also reused its cached native app and a parked simulator.&lt;/p&gt;&lt;figure class=&quot;body-video&quot;&gt;&lt;video controls playsinline preload=&quot;metadata&quot;&gt;&lt;source src=&quot;https://cdn.sanity.io/files/edo1vll6/production/295a03d5d8ddfe60220667114a0ecbcb5c022d08.mp4&quot; type=&quot;video/mp4&quot; /&gt;&lt;/video&gt;&lt;/figure&gt;&lt;p&gt;Timings start at the first recorded agent activity and end at the verified screenshot, excluding runner startup and initial reasoning before that activity. The video aligns two separately recorded runs on that clock, with waiting sped up. Vanilla is on the left; Stim is on the right.&lt;/p&gt;&lt;p&gt;We also tested native changes and launch errors. In the recorded GPT-5.6 Sol iOS native-change comparison, Stim reached screenshot proof in &lt;strong&gt;4m10s versus 12m1s&lt;/strong&gt;. In the iOS launch-error comparison, the agent had to diagnose an injected error, fix it, and verify the repaired screen: &lt;strong&gt;3m18s with Stim versus 11m54s&lt;/strong&gt; with local tooling. On Android, the same model completed launch-error recovery in &lt;strong&gt;3m04s with Stim versus 9m02s&lt;/strong&gt; with local tooling. These include the work from the first recorded agent activity through verification, not just compilation or the first error message.&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/7feaaa8344875ea930d6699b7600a9573bee0384-2400x1640.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;Bar chart of time from task to verified screen for GPT-5.6 Sol on iOS and Android, comparing Stim with vanilla tooling on a JavaScript change, a native change, and launch-error recovery&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;/figure&gt;&lt;p&gt;The &lt;a href=&quot;https://appandflow.github.io/stim/benchmarks&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;benchmark viewer&lt;/a&gt; includes the methodology, command logs, and proof.&lt;/p&gt;&lt;h2 id=&quot;who-its-for&quot;&gt;Who it’s for&lt;/h2&gt;&lt;p&gt;Stim is most useful when agents regularly create worktrees, run in parallel, or need to build and validate changes. If you work in one checkout with an already-running Expo development client and rarely change native code, your existing workflow may be enough.&lt;/p&gt;&lt;p&gt;Stim is available as an open-source release. Add the CLI and agent skill, then ask your agent to build and run the app.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://appandflow.github.io/stim/docs/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Get started&lt;/a&gt; · &lt;a href=&quot;https://github.com/appandflow/stim&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;&lt;p&gt;We’d love to hear where it helps—and where your agent still gets stuck.&lt;/p&gt;</content:encoded><dc:creator>Janic Duplessis</dc:creator></item><item><title>iOS Asset Catalogs Are Coming to React Native</title><link>https://blog.appandflow.com/ios-asset-catalogs-are-coming-to-react-native</link><guid isPermaLink="true">https://blog.appandflow.com/ios-asset-catalogs-are-coming-to-react-native</guid><description>React Native 0.88 can ship the images you require() in an iOS asset catalog. How it works, and why it makes image lookup 15x faster and decoding about 2x faster.</description><pubDate>Tue, 11 Aug 2026 17:41:37 GMT</pubDate><content:encoded>&lt;p&gt;In the fall of 2020, I was building an App Clip. If you haven&amp;#x27;t had the pleasure: an App Clip is a miniature version of your app that launches instantly from a QR code or a link, and Apple enforces the miniature part with an app bundle size budget. Back then that budget was 10 MB after thinning (Apple has since loosened it to 15 MB, or 100 MB on iOS 17+ if your Clip is only launched digitally).&lt;/p&gt;&lt;p&gt;The app was built with React Native, and 10 MB is not a lot. For scale, I built today&amp;#x27;s React Native hello world while writing this: Release, arm64, no features, no images, no product. The app bundle came out at 10.6 MB, and Hermes is 4.8 MB of that. An empty React Native app in 2026 misses the 2020 App Clip budget on its own. It was leaner back then, to be fair: iOS ran JavaScriptCore, a system framework that costs zero bundle bytes, and the New Architecture didn&amp;#x27;t exist yet. But the floor was high enough that every megabyte I could find mattered, so I went hunting for ways to shrink the bundle. Poking through the built &lt;code class=&quot;inline-code&quot;&gt;.app&lt;/code&gt;, I noticed the images: every asset the app &lt;code class=&quot;inline-code&quot;&gt;require()&lt;/code&gt;s was copied in as loose files at every scale it ships, 1x, 2x, &lt;em&gt;and&lt;/em&gt; 3x. A 3x phone will never draw the first two, so two of the three scale variants were dead weight, and app thinning, the mechanism that&amp;#x27;s supposed to strip unused scales, can&amp;#x27;t touch loose files. That sent me researching how native apps handle assets, and the answer was: asset catalogs.&lt;/p&gt;&lt;p&gt;In an asset catalog, images are organized by scale and given a unique identifier that can be referenced in code later. They then get compiled into a single indexed file, memory-mapped at runtime instead of split across the filesystem, and sliced per device at download time so we don’t ship scales a phone can&amp;#x27;t draw. All of it built to make asset loading as efficient as possible, but none of it applied to React Native&amp;#x27;s images.&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/598db43b68fe0e6696376ee9f2dfc1bc7e46571f-1292x928.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;An imageset inside an asset catalog open in Xcode, with separate slots for the 1x, 2x and 3x versions of one image.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;figcaption&gt;Asset catalog in Xcode&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;I started working on this, shipped it in my RN fork, and opened &lt;a href=&quot;https://github.com/facebook/react-native/pull/30129&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #30129&lt;/a&gt; upstream, but I never finished addressing the review feedback and it stalled short of merging.&lt;/p&gt;&lt;p&gt;Since last year I&amp;#x27;ve been back consulting at App &amp;amp; Flow, where open source is part of what I do. Eventually I got back to this PR. Some parts had to be reworked, and it ended up as a much better design than my original. What follows is how it works, from the CLI down to the image loader.&lt;/p&gt;&lt;h2 id=&quot;how-images-load-today&quot;&gt;How images load today&lt;/h2&gt;&lt;p&gt;It starts at build time. When Xcode runs the &amp;quot;Bundle React Native code and images&amp;quot; phase, the CLI asks Metro for every image the app &lt;code class=&quot;inline-code&quot;&gt;require()&lt;/code&gt;s and copies each one into the app bundle as individual files, one per scale: &lt;code class=&quot;inline-code&quot;&gt;img/logo.png&lt;/code&gt; ships as &lt;code class=&quot;inline-code&quot;&gt;assets/img/logo.png&lt;/code&gt;, &lt;code class=&quot;inline-code&quot;&gt;assets/img/logo@2x.png&lt;/code&gt;, and &lt;code class=&quot;inline-code&quot;&gt;assets/img/logo@3x.png&lt;/code&gt;, sitting next to the JS bundle.&lt;/p&gt;&lt;p&gt;At runtime, when the app loads an image via &lt;code class=&quot;inline-code&quot;&gt;require(&amp;#x27;./img/logo.png&amp;#x27;)&lt;/code&gt;, it calls &lt;code class=&quot;inline-code&quot;&gt;[UIImage imageNamed:]&lt;/code&gt; with the bundle-relative path. The scale is already resolved on the JS side, so the name arrives with the scale suffix and extension already attached (&lt;code class=&quot;inline-code&quot;&gt;assets/img/logo@2x.png&lt;/code&gt;), and UIKit goes hunting through the bundle for a file matching it.&lt;/p&gt;&lt;h2 id=&quot;meanwhile-at-apple-thirteen-years-ago&quot;&gt;Meanwhile, at Apple, thirteen years ago&lt;/h2&gt;&lt;p&gt;Xcode 5 shipped something called an asset catalog. It is an &lt;code class=&quot;inline-code&quot;&gt;.xcassets&lt;/code&gt; directory, one &lt;code class=&quot;inline-code&quot;&gt;.imageset&lt;/code&gt; folder per image, each holding the image files and a &lt;code class=&quot;inline-code&quot;&gt;Contents.json&lt;/code&gt;:&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;images&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;filename&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;logo.png&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;idiom&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;universal&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;scale&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;1x&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;filename&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;logo@2x.png&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;idiom&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;universal&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;scale&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;2x&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;filename&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;logo@3x.png&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;idiom&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;universal&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;scale&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;3x&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;info&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;xcode&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;The useful part happens at build time: a tool named &lt;code class=&quot;inline-code&quot;&gt;actool&lt;/code&gt; compiles all of it into a single &lt;code class=&quot;inline-code&quot;&gt;Assets.car&lt;/code&gt;, a binary archive with an index mapping each name to its &lt;em&gt;renditions&lt;/em&gt;, the per-scale, per-idiom (iPhone vs iPad vs Mac), per-appearance variants of that image. At runtime, &lt;code class=&quot;inline-code&quot;&gt;UIImage(named:)&lt;/code&gt; does a best-match over rendition attributes to a memory-mapped index of all images.&lt;/p&gt;&lt;p&gt;If you&amp;#x27;re curious what&amp;#x27;s inside one, &lt;code class=&quot;inline-code&quot;&gt;xcrun assetutil --info&lt;/code&gt; dumps the inventory, one entry per rendition:&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; xcrun&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; assetutil&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --info&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; RNAssets.bundle/Assets.car&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  { &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Platform&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;ios&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;PlatformVersion&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;15.1&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;SchemaVersion&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; 2, … },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;AssetType&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;     :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;Image&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;Name&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;          :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;header&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;RenditionName&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;header@2x.png&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;Scale&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;         :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; 2,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;Idiom&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;         :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;universal&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;Compression&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;   :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;deepmap2&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;PixelWidth&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; 160,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;PixelHeight&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;   :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; 160,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;    &quot;SizeOnDisk&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    :&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; 830&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  …&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Then there&amp;#x27;s the part that started this whole story: &lt;strong&gt;app thinning&lt;/strong&gt;. When the App Store prepares your build for a specific device, catalog contents get sliced. An iPhone 17 downloads only the 3x renditions; the 1x and 2x bytes never leave Apple&amp;#x27;s servers. Loose files get no such treatment.&lt;/p&gt;&lt;p&gt;Every iOS app template since 2013 has an asset catalog. React Native apps have one too… for the app icon. Meanwhile, every image you actually &lt;code class=&quot;inline-code&quot;&gt;require()&lt;/code&gt; sits outside it as loose files, unindexed and unsliced.&lt;/p&gt;&lt;h2 id=&quot;two-designs-six-years-apart&quot;&gt;Two designs, six years apart&lt;/h2&gt;&lt;p&gt;The 2020 version worked, but it made &lt;em&gt;you&lt;/em&gt; do part of the work. You created an &lt;code class=&quot;inline-code&quot;&gt;RNAssets.xcassets&lt;/code&gt; in your Xcode project by hand and committed it. You reordered your build phases so the bundle script ran before the catalog got compiled, since the script is what fills the catalog in. New apps needed a template change to get any of it.&lt;/p&gt;&lt;p&gt;Xcode also kept moving underneath it. By the time I came back, the asset-symbol generator that shipped in Xcode 15 formed a dependency cycle with that phase reordering, and breaking the cycle meant setting &lt;code class=&quot;inline-code&quot;&gt;GENERATE_ASSET_SYMBOLS = NO&lt;/code&gt; on the whole target. That would have broken &lt;code class=&quot;inline-code&quot;&gt;UIImage(resource:)&lt;/code&gt; for the app&amp;#x27;s own images. Dealbreaker here.&lt;/p&gt;&lt;p&gt;That is a lot of ceremony to ask of every app, and every step of it was a way for someone&amp;#x27;s build to go wrong. It&amp;#x27;s a big piece of why the PR stalled: the feature was fine, the migration was not.&lt;/p&gt;&lt;p&gt;So I threw the recipe out and went looking for a better one. This round I could iterate on designs with AI instead of prototyping each by hand, and the one that I ended up choosing stands out for how simple it is.&lt;/p&gt;&lt;p&gt;The idea is that React Native manages its own asset catalog. Nothing gets committed. No build phases move. Your app&amp;#x27;s own &lt;code class=&quot;inline-code&quot;&gt;Images.xcassets&lt;/code&gt; (icons, launch images, &lt;code class=&quot;inline-code&quot;&gt;UIImage(resource:)&lt;/code&gt; symbols) is never touched.&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/639a70f872c57681e771aaf3dc66d60797439d44-635x848.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;Flowchart. A require of logo.png branches on whether RCTUseAssetCatalog is set in Info.plist. Without the key, the build copies loose files and the runtime does a per-image filesystem lookup. With the key, the CLI emits imagesets into a staging directory, actool compiles RNAssets.bundle and Assets.car, and the runtime does one catalog lookup with imageNamed:inBundle:.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;/figure&gt;&lt;p&gt;That leaves exactly one thing for an app to do: turn it on. The whole feature sits behind a flag, a single &lt;code class=&quot;inline-code&quot;&gt;Info.plist&lt;/code&gt; key, so existing apps keep the old behavior until they opt in:&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;RCTUseAssetCatalog&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;That&amp;#x27;s the entire migration. Getting there took three coordinated pieces: the CLI writes a catalog, the build compiles it, the runtime reads it.&lt;/p&gt;&lt;h2 id=&quot;part-1-the-cli-writes-a-catalog&quot;&gt;Part 1: the CLI writes a catalog&lt;/h2&gt;&lt;p&gt;Fun fact: this half of the feature merged into the CLI back in 2022, midway through the PR&amp;#x27;s sleep. It has shipped inside every React Native app&amp;#x27;s tooling since, dormant, waiting for a native side that never came. Nothing ever passed it the flag.&lt;/p&gt;&lt;p&gt;Here&amp;#x27;s what happens when something finally passes it. Given &lt;code class=&quot;inline-code&quot;&gt;--asset-catalog-dest&lt;/code&gt;, &lt;code class=&quot;inline-code&quot;&gt;saveAssets&lt;/code&gt; in &lt;code class=&quot;inline-code&quot;&gt;@react-native/community-cli-plugin&lt;/code&gt; checks whether the asset is a &lt;code class=&quot;inline-code&quot;&gt;png&lt;/code&gt;, &lt;code class=&quot;inline-code&quot;&gt;jpg&lt;/code&gt;, or &lt;code class=&quot;inline-code&quot;&gt;jpeg&lt;/code&gt; and routes it through &lt;code class=&quot;inline-code&quot;&gt;getImageSet&lt;/code&gt;, which writes exactly the &lt;code class=&quot;inline-code&quot;&gt;.imageset&lt;/code&gt; structure shown above, files copied in, &lt;code class=&quot;inline-code&quot;&gt;Contents.json&lt;/code&gt; generated, one entry per scale. Everything else (gif, webp, fonts) takes the loose-file path, same as before.&lt;/p&gt;&lt;p&gt;That list maps to what an imageset will actually hold: gif and webp aren&amp;#x27;t valid imageset contents. Put a &lt;code class=&quot;inline-code&quot;&gt;.gif&lt;/code&gt; in an imageset and actool responds &amp;quot;that file does not have a valid extension&amp;quot;, leaves it out of the &lt;code class=&quot;inline-code&quot;&gt;.car&lt;/code&gt;, and exits 0 anyway. Since the runtime does a catalog lookup and nothing else, a rendition that quietly failed to compile is a blank space on screen. So the CLI routes only the formats catalogs actually support, and everything else keeps shipping the way it always has.&lt;/p&gt;&lt;p&gt;The function that matters is the one that &lt;em&gt;names&lt;/em&gt; the imagesets. Asset catalogs are a flat namespace; Metro assets live in folders. So the CLI flattens the path into an identifier:&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; getResourceIdentifier&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;asset&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; PackagerAsset&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; folderPath&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt; getBasePath&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(asset);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; `${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;folderPath&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}/${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;asset&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;toLowerCase&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold&quot;&gt;\/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;_&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)          &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;// encode folder structure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;a-z0-9_]&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;g&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;// drop illegal chars&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;(?:assets&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#DBEDFF&quot;&gt;assetsunstable_path)_&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;); &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;// strip &quot;assets_&quot; or &quot;assetsunstable_path_&quot; prefix&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;code class=&quot;inline-code&quot;&gt;img/logo.png&lt;/code&gt; → imageset &lt;code class=&quot;inline-code&quot;&gt;img_logo&lt;/code&gt;. The scheme predates the feature; it&amp;#x27;s how Android has named RN&amp;#x27;s &lt;code class=&quot;inline-code&quot;&gt;drawable&lt;/code&gt; resources all along.&lt;/p&gt;&lt;p&gt;Remember this function. The native side has to reproduce its output byte for byte, in a different language, from a different input.&lt;/p&gt;&lt;h2 id=&quot;part-2-the-build-compiles-it&quot;&gt;Part 2: the build compiles it&lt;/h2&gt;&lt;p&gt;&lt;code class=&quot;inline-code&quot;&gt;react-native-xcode.sh&lt;/code&gt; sources a small new script, &lt;code class=&quot;inline-code&quot;&gt;scripts/xcode/asset-catalog.sh&lt;/code&gt;. Its first job is deciding whether the feature is on, and it does that by reading &lt;code class=&quot;inline-code&quot;&gt;RCTUseAssetCatalog&lt;/code&gt; out of the app&amp;#x27;s Info.plist.&lt;/p&gt;&lt;p&gt;If the key is on, the script stages an empty &lt;code class=&quot;inline-code&quot;&gt;RNAssets.xcassets&lt;/code&gt; in a scratch directory under &lt;code class=&quot;inline-code&quot;&gt;DERIVED_FILE_DIR&lt;/code&gt;, hands it to the bundler, and afterwards compiles it with &lt;code class=&quot;inline-code&quot;&gt;actool&lt;/code&gt; into &lt;code class=&quot;inline-code&quot;&gt;RNAssets.bundle&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The end result, in the built app:&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/5ac5b65d2eae949552005bd098b4ff459ea1672d-2259x787.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;Before and after file trees for MyApp.app. Before, assets/img holds logo.png, logo@2x.png, logo@3x.png, header.png and more, one file per image per scale. After, RNAssets.bundle holds a single Assets.car plus an Info.plist, with every png and jpg indexed and compressed inside it.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;/figure&gt;&lt;p&gt;Non-catalog assets (gif, webp, fonts) still ship loose under &lt;code class=&quot;inline-code&quot;&gt;assets/&lt;/code&gt;, same as before.&lt;/p&gt;&lt;h2 id=&quot;part-3-the-runtime-reads-it&quot;&gt;Part 3: the runtime reads it&lt;/h2&gt;&lt;p&gt;The runtime side is three small functions in &lt;code class=&quot;inline-code&quot;&gt;RCTUtils.mm&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;inline-code&quot;&gt;RCTUseAssetCatalog()&lt;/code&gt; reads the plist key once, using &lt;code class=&quot;inline-code&quot;&gt;dispatch_once&lt;/code&gt;. This is the feature flag. If it&amp;#x27;s on, the assets are in the catalog: the bundling script reads the same key.&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;inline-code&quot;&gt;RCTAssetCatalogBundle()&lt;/code&gt; resolves &lt;code class=&quot;inline-code&quot;&gt;RNAssets.bundle&lt;/code&gt; once and caches it. If the bundle is missing, the loader logs an error and exits rather than passing a nil bundle to &lt;code class=&quot;inline-code&quot;&gt;imageNamed:inBundle:&lt;/code&gt;, because nil there means &amp;quot;search the main bundle&amp;quot;, which could resolve some unrelated image from the app&amp;#x27;s own catalog and put the wrong picture on screen.&lt;/p&gt;&lt;p&gt;Then the fun one. &lt;code class=&quot;inline-code&quot;&gt;RCTAssetCatalogNameForURL&lt;/code&gt; takes&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;file:///.../MyApp.app/assets/img/logo@2x.png&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;and must produce &lt;code class=&quot;inline-code&quot;&gt;img_logo&lt;/code&gt;: exactly what that JS regex chain produces, byte for byte, or the lookup misses. My 2020 version did the obvious port: split the path, run &lt;code class=&quot;inline-code&quot;&gt;NSRegularExpression&lt;/code&gt;, lowercase, join. It works, but this is a pretty hot code path. This is 2026, so after adding a bunch of tests I had AI help me rewrite it around a stack buffer, avoiding the extra allocations. About 2.2× faster than the regex version, same output.&lt;/p&gt;&lt;p&gt;Now that we have the name, UIKit can resolve the image in the asset catalog of the RN bundle. One call, no need to handle the scale.&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;UIImage &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;image &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [UIImage &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;imageNamed:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;catalogName&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;                            inBundle:&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;assetCatalogBundle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;       compatibleWithTraitCollection:nil&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Not everything goes through this lookup. The loader only takes the catalog path for URLs that point inside the app bundle&amp;#x27;s &lt;code class=&quot;inline-code&quot;&gt;assets/&lt;/code&gt; folder with a png/jpg/jpeg extension. Everything else, like the gif and webp files that shipped loose, or assets delivered outside the main app bundle by OTA update systems, never matches that check and falls through to the regular loader, untouched. That&amp;#x27;s also why OTA updates just work.&lt;/p&gt;&lt;h2 id=&quot;the-numbers&quot;&gt;The numbers&lt;/h2&gt;&lt;p&gt;RNTester, Release build, cold first load, the same 11 images in both modes, instrumented directly at &lt;code class=&quot;inline-code&quot;&gt;RCTImageFromLocalAssetURL&lt;/code&gt;. The median image resolved in &lt;strong&gt;719 µs&lt;/strong&gt; from loose files and &lt;strong&gt;47 µs&lt;/strong&gt; from the catalog, about &lt;strong&gt;15× faster&lt;/strong&gt;.&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/85ab25389ecc15ff3a1a474684f67ed175fc2351-1720x1280.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;Chart comparing cold image load time for 11 images, loose files against asset catalog. The catalog is far faster on every image.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;figcaption&gt;Cold image load: loose files vs asset catalog&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Note that what is measured here is the time to resolve and open each image, not decode it. Since asset catalogs are a single memory-mapped file, only the first load pays to map the index; after that a lookup is an index hit in the already-mapped file, while loose files pay a filesystem search on the first load of every image.&lt;/p&gt;&lt;p&gt;That leaves the other half of the load: the decode. Catalog renditions aren&amp;#x27;t PNGs. &lt;code class=&quot;inline-code&quot;&gt;actool&lt;/code&gt; re-encodes each one into Apple&amp;#x27;s own format, the &lt;code class=&quot;inline-code&quot;&gt;deepmap2&lt;/code&gt; visible in the &lt;code class=&quot;inline-code&quot;&gt;assetutil&lt;/code&gt; dump above, so decoding takes a different code path too. I measured that separately, outside React Native: the same 26 images shipped twice in one app, once as loose files and once compiled into a car, each loaded cold and forced through a full decode into an identical bitmap context.&lt;/p&gt;&lt;figure class=&quot;body-image&quot;&gt;&lt;img src=&quot;https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=600&amp;q=100&amp;fit=max&quot; srcset=&quot;https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=600&amp;q=100&amp;fit=max 600w, https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=900&amp;q=100&amp;fit=max 900w, https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=1200&amp;q=100&amp;fit=max 1200w, https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=1500&amp;q=100&amp;fit=max 1500w, https://cdn.sanity.io/images/edo1vll6/production/6b08e802cfca72b73f45bedcebc2c1b21ef7c9a9-2260x1490.png?w=1800&amp;q=100&amp;fit=max 1800w&quot; sizes=&quot;(max-width: 652px) calc(100vw - 32px), 600px&quot; alt=&quot;Bar chart of decode time, loose PNG file against asset catalog in deepmap2 format, split into large images and icon sized images. The catalog decodes faster on nearly every image, for example 145.2 ms against 33.2 ms on the largest one.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;&lt;/figure&gt;&lt;p&gt;The catalog wins there too, and by more than it wins on lookup: 247 ms versus 74 ms to decode all 26 images, a median of 2.2× per image, rising to 2.9× on the multi-megapixel ones and 4.4× on a 32-megapixel PNG. Two of the smallest icons came out a wash; every other image decoded faster. That benchmark ran on the simulator rather than on a phone, so read the magnitudes as directional, but the direction is not in doubt: on image-heavy screens the decode saving is the larger half of the win.&lt;/p&gt;&lt;h2 id=&quot;what-about-expo&quot;&gt;What about Expo?&lt;/h2&gt;&lt;p&gt;Expo apps bundle with &lt;code class=&quot;inline-code&quot;&gt;expo export:embed&lt;/code&gt; instead of the React Native community CLI, so I checked whether any of this carries over. It turns out Expo&amp;#x27;s CLI already implements &lt;code class=&quot;inline-code&quot;&gt;--asset-catalog-dest&lt;/code&gt;, imageset writing and all: the CLI half of the feature merged upstream in 2022, and Expo mirrored it when they built &lt;code class=&quot;inline-code&quot;&gt;export:embed&lt;/code&gt;. And a prebuilt Expo app&amp;#x27;s bundle phase runs the same &lt;code class=&quot;inline-code&quot;&gt;react-native-xcode.sh&lt;/code&gt; from React Native, just pointed at Expo&amp;#x27;s CLI. The whole pipeline is already wired, waiting on an SDK that targets React Native 0.88 or newer, the release this feature ships in (more on that below).&lt;/p&gt;&lt;p&gt;Once that ships, turning it on should be one entry in app config, no config plugin needed, since prebuild writes &lt;code class=&quot;inline-code&quot;&gt;ios.infoPlist&lt;/code&gt; values into the generated Info.plist as the literal values the build script reads:&lt;/p&gt;&lt;figure class=&quot;code-block&quot;&gt;&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;expo&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;ios&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;infoPlist&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;RCTUseAssetCatalog&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;From there, Expo could flip it on for everyone by making the key a prebuild default, with &lt;code class=&quot;inline-code&quot;&gt;ios.infoPlist&lt;/code&gt; as the opt-out. I haven&amp;#x27;t run an end-to-end Expo build yet, so consider this well-informed optimism rather than a promise.&lt;/p&gt;&lt;h2 id=&quot;shipping-in-088&quot;&gt;Shipping in 0.88&lt;/h2&gt;&lt;p&gt;We can now finally load local images the exact same way as native apps, without the Xcode drag and drop.&lt;/p&gt;&lt;p&gt;App thinning applies too: a thinned archive slices &lt;code class=&quot;inline-code&quot;&gt;RNAssets.bundle&lt;/code&gt; exactly like the app&amp;#x27;s own catalog, so migrated apps stop shipping 1x and 2x bytes to 3x devices.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/react-native/pull/30129&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #30129&lt;/a&gt;: opened October 2020, merged July 23, 2026 as &lt;code class=&quot;inline-code&quot;&gt;26769a00b2&lt;/code&gt;. In between, React Native replaced its architecture, Xcode grew a new asset pipeline, and the implementation had to be reworked around both, coming out smaller than the original. Thanks to Pieter De Baets (&lt;a href=&quot;https://github.com/javache&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@javache&lt;/a&gt;) for reviewing both iterations, six years apart, and for shepherding it through Meta&amp;#x27;s import.&lt;/p&gt;&lt;p&gt;It missed the 0.87 branch cut by two weeks, so it ships in &lt;strong&gt;React Native 0.88&lt;/strong&gt;, opt-in. Enabling it by default for new apps is a follow-up in &lt;code class=&quot;inline-code&quot;&gt;react-native-community/template&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;As for the App Clip that started all this: it never shipped. The PR outlived the product that needed it.&lt;/p&gt;&lt;p&gt;If you find this interesting, this is the kind of work we do at App &amp;amp; Flow. We strive to push the boundaries of what is possible to do with React Native.&lt;/p&gt;</content:encoded><dc:creator>Janic Duplessis</dc:creator></item></channel></rss>