The Simpcn handbook
React Native with Expo
Experimental CDN delivery for static PNG/JPEG images, iOS and Android variants, and screen densities.
Keep your image references
React Native normally packages local image files with the app. Simpcn can replace supported image sources with verified CDN sources during an iOS or Android production build, while leaving your application source unchanged. Development keeps Metro’s normal local image handling.
This integration uses Expo’s Metro configuration. It is not support for bare React Native or Expo web. The tested fixture uses Expo 58.0.14, expo-image 58.0.15, React Native 0.88.0, and React 19.3.0.
Static images: imports and require()
A static image is a local file whose path is known at build time. Both forms below are supported for PNG and JPEG images inside the managed directory:
import { Image } from 'react-native';
import logo from './assets/cdn/logo.png';
<Image source={logo} />;
<Image source={require('./assets/cdn/logo.png')} />;Supported consumers are named imports of React Native Image and ImageBackground, and Image from expo-image. Import aliases are recognized. The build verifies each selected file and emits its CDN URL and image dimensions.
An image import is removed only when all its uses have been rewritten. If the same import is used by a fallback, another consumer, or other application logic, its local asset module remains bundled. Arbitrary strings and variables containing image paths are not automatically rewritten.
Separate iOS and Android images
Use different artwork on each platform without changing the component reference. Metro-style suffixes identify the platform:
assets/cdn/
payment.png # Generic fallback
payment.ios.png # iOS artwork
payment.android.png # Android artwork
// Inside a component beside assets/:
<Image source={require('./assets/cdn/payment.png')} />An iOS production build uses the iOS family’s CDN URLs; Android uses the Android family. A platform without its own family uses the generic family. Once a platform-specific family exists, its densities are selected as a group: missing densities are not filled from generic artwork.
.native families currently remain local. Keep filenames in Metro’s order when combining platform and density: payment@2x.ios.png.
Sharp images with @2x and @3x
Phones have different numbers of physical pixels in the same screen space. Density variants provide sharper copies of an image while keeping its displayed size consistent.
| File | Physical pixels | Logical display size |
|---|---|---|
logo.png | 24 × 24 | 24 × 24 |
logo@2x.png | 48 × 48 | 24 × 24 |
logo@3x.png | 72 × 72 | 24 × 24 |
Keep using require('./logo.png') or the equivalent static import. A small generated helper reads React Native’s device pixel ratio and chooses the first available density at least as large as the device needs. If none is large enough, it uses the largest available. A density of 2.625 selects @3x when available.
You supply the variant files; Simpcn does not resize images or generate @2x/@3x copies. The adapter calculates logical dimensions from pixels divided by density. Inconsistent dimensions, duplicate densities, or missing verification for a selectable sibling fail the build. Families containing only @2x/@3x files can still use the base reference.
assets/cdn/
logo.ios.png # 24 × 24 pixels
logo@2x.ios.png # 48 × 48 pixels
logo.android.png # 24 × 24 pixels
logo@3x.android.png # 72 × 72 pixelsConnect the experimental adapter
npm i -D simpcnRun the CLI in your Expo application directory. Init can install @simpcn/expo using your project’s package manager:
npx simpcn init --framework expoInit detects Expo before Next.js or Vite, defaults the managed root to assets/cdn, and adds native release guidance. It preserves your Metro configuration and prints the wrapper instructions:
const { getDefaultConfig } = require('expo/metro-config');
const { withSimpcn } = require('@simpcn/expo/metro');
const config = getDefaultConfig(__dirname);
// Keep your existing Metro configuration here.
module.exports = withSimpcn(config);Choose a managed directory and release
import type { UserConfig } from 'simpcn';
export default {
root: 'assets/cdn',
exclude: ['essential/**', 'fallback.png'],
stateFile: 'simpcn/deployment-state.json',
manifestFile: 'simpcn/.build/manifest.json',
storage: {
provider: 'r2',
endpoint: 'https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com',
bucket: 'YOUR_EXISTING_BUCKET',
region: 'auto',
prefix: 'releases/mobile-v1',
},
publicBaseUrl: 'https://YOUR_PUBLIC_HOST/releases/mobile-v1/',
} satisfies UserConfig;Use a unique, immutable release prefix and matching public URL. Replacing bytes at a URL used by an older installed app can change that app’s images too. Keep older release namespaces available for installed builds and rollback, and use the same fixed release identifier for migration and bundling.
For R2, the API endpoint is the account origin without the bucket name. The public URL includes the storage prefix once. Credentials stay in local environment files or a secret store. Storage and R2 setup →
Keep essential images local
Keep app icons, splash screens, offline images, and essential fallbacks outside the managed root or add exclusions relative to it. Excluded images retain their local sources. If any selectable density sibling is excluded, that selected family stays local.
CDN images may be unavailable on first launch without a network connection. This adapter does not add automatic fallback or guaranteed offline caching. Fonts, audio/video, SVG components, placeholders, and asset-preloading calls are outside the automatic image transform.
Migrate, then export
npx simpcn plan
npx simpcn migrate
npx expo export --platform ios
npx expo export --platform androidCommit the deployment state with matching assets, config, source, and lockfile. Production compilation checks local verification records and does not upload files or require storage credentials. Generated workers and the mobile density helper live under ignored simpcn/.build/expo/.
Changed images must be migrated again. Restart Metro after changing assets, state, or configuration; fresh export processes invalidate existing Simpcn caches without requiring a cache clear. Live dev-server invalidation has not been established.
What has been verified
Tests cover binding-aware transforms, import retention, logical dimensions, platform precedence, density selection, exclusions, stale siblings, and packed package installation. Real Android/iOS exports contain the expected CDN sources and omit transformed images from the asset graph; mixed-use and excluded assets remain bundled.
These export checks do not establish actual device rendering, downloads, caching, native/EAS release behavior, or OTA update/rollback safety. Namespace component imports, custom wrappers, dynamic requires, duplicate source props, and ambiguous spread props remain outside automatic rewriting. Expo web and bare React Native need separate integrations and validation.