Media Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Classify the job first: Capture, display, playback, and editing use different modules - do not default to one package for every screen.
Install with npx expo install: Pin expo-image, expo-camera, and expo-video to SDK 57–tested native binaries - avoid npm install @latest on native media packages.
Use expo-image for remote URLs: Disk and memory caching, BlurHash placeholders, and transitions beat raw React Native Image in feeds.
Compress before upload: Run expo-image-manipulator (max width 1280–1920, JPEG ~0.75) before preview and multipart upload - store URIs, not base64, in state.
Seed disk cache after capture: Image.writeToCacheAsync(localUri, cacheKey) pairs with the same cacheKey in source for instant list thumbnails after picker or camera output.
Set recyclingKey in virtualized lists: FlashList and FlatList rows must reset image cells when IDs change - prevents avatar flash from recycled views.
Prefetch the next screen's URLs: Image.prefetch(urls, { cachePolicy: "memory-disk" }) during idle time after page-one load - cap concurrency.
Request camera permission in context: Explain why capture is needed, then call requestPermission - cold-start permission prompts increase denials.
Offer library fallback: Pair expo-camera with expo-image-picker so deny paths still attach photos - see expo-camera & expo-image-picker.
Only one active CameraView: Unmount or gate with useIsFocused when tabs or modals hide the preview - two sessions cause black preview or crashes.
Debounce barcode scans: onBarcodeScanned fires continuously - 1–2 second debounce and unset handler after success before navigating.
Wait for onCameraReady: takePictureAsync before ready throws or returns blank frames on Android - gate the shutter button.
Turn torch off on unmount: enableTorch drains battery and blinds warehouse cameras left enabled in background tabs.
Pause camera preview under sheets: pausePreview() while overlays cover the viewfinder - preview GPU cost is continuous.
Adopt expo-video for new playback: useVideoPlayer + VideoView with automatic disposal - retire expo-av on touched screens.
Subscribe to player events: player.playing does not re-render React - use useEvent or useEventListener from expo for controls and status UI.
Pause video on navigation blur: useFocusEffect cleanup calling player.pause() prevents audio bleed and background review complaints.
Mute feed autoplay by default: Grid clips should set player.muted = true until explicit user unmute - unexpected audio is a top rejection theme.
Configure background audio and PiP at build time: expo-video config plugin supportsBackgroundPlayback and supportsPictureInPicture require rebuild - runtime props alone are insufficient.
Preload adjacent video sources: Second useVideoPlayer(nextUri) fills buffers before replaceAsync - one VideoView per player on Android.
Cap Skia path complexity: Memoize Skia.Path builds; downsample chart series - GPU drawing still costs JS preparation time.
Use Skia for charts, not list thumbnails: expo-image owns raster caching; Skia excels at ring gauges, sparklines, and composited overlays.
Cluster map markers: Do not mount thousands of <Marker> nodes - supercluster or server aggregation with tracksViewChanges={false} on settled custom pins.
Never embed MapView in every list row: Open a full-screen map route or show static snapshots - embedded maps choke scroll and GPU.
Profile on mid-tier Android: iPhone Pro cameras and simulators hide preview jank, decoder stalls, and OOM - run the media checklist on a Pixel A-series before release.
expo-image-manipulator before any expo-image or upload pipeline.expo-image, expo-camera, and basic expo-video.react-native-maps, Skia, PiP, and background audio verification.cachePolicy="none" or rotate cacheKey when the signature expires - stale disk entries cause 403 loops.await Image.clearMemoryCache();
await Image.clearDiskCache();Run from settings or session teardown - not on every screen focus.
writeToCacheAsyncStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026