Nitro Image is a superfast Image core type and view component for React Native, built with Nitro!
- Powered by Nitro Modules for highly efficient native bindings! 🔥
- Instance-based
Imagetype with byte-buffer pixel data access 🔗 - Supports in-memory image operations like resizing and cropping without saving to file 📐
- Supports deferred
ImageLoadertypes to optimize for displaying large lists of Images ⏳ - Fast Web Image loading and caching using SDWebImage (iOS) and Coil (Android) 🌎
- ThumbHash support for elegant placeholders 🖼️
function App() {
return (
<NitroImage
image={{ filePath: '/tmp/image.jpg' }}
style={{ width: 400, height: 400 }}
/>
)
}Install react-native-nitro-image from npm:
npm i react-native-nitro-image
npm i react-native-nitro-modules
cd ios && pod installNote
Since NitroImage is built with Nitro Views, it requires the new architecture to be enabled.
To keep NitroImage super lightweight, it does not ship a web image loader and caching system. If you want to load images from the web, install react-native-nitro-web-image as well:
npm i react-native-nitro-web-image
cd ios && pod installThen, since SDWebImage does not enable modular headers for static linkage, you need to enable those yourself in your app's Podfile:
target '…' do
config = use_native_modules!
# Add this line:
pod 'SDWebImage', :modular_headers => trueThe simplest way to load an Image is to use the exported loadImage(…) method:
const webImage = await loadImage({ url: 'https://picsum.photos/seed/123/400' })
const fileImage = await loadImage({ filePath: 'file://my-image.jpg' })
const resourceImage = await loadImage({ resource: 'my-image.jpg' })
const symbolImage = await loadImage({ symbol: 'star' })
const requireImage = await loadImage(require('./my-image.jpg'))Under the hood, this uses the native methods from Images or WebImages:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const fileImage = await Images.loadFromFileAsync('file://my-image.jpg')
const resourceImage = Images.loadFromResources('my-resource.jpg')
const symbolImage = Images.loadFromSymbol('star')When loading from a remote URL, you can tweak options such as priority:
const image1 = await WebImages.loadFromURLAsync(URL1, { priority: 'low' })
const image2 = await WebImages.loadFromURLAsync(URL2, { priority: 'high' })If you know what Images are going to be rendered soon, you can pre-load them using the preload(...) API:
WebImages.preload(profilePictureLargeUrl)A React Native require(…) returns a resource-ID. In debug, resources are streamed over Metro (localhost://…), while in release, they are embedded in the resources bundle.
NitroImage wraps those APIs so you can just pass a require(…) to useImage(…), useImageLoader(…), or <NitroImage /> directly:
const image = useImage(require('./image.png'))The Image type can be converted to- and from- an ArrayBuffer, which gives you access to the raw pixel data in an RGB format:
const image = ...
const pixelData = await image.toRawPixelData()
const sameImageCopied = await Images.loadFromRawPixelData(pixelData)The Image type can be encoded to- and decoded from- an ArrayBuffer using a container format like jpg, png or heic:
const image = ...
const imageData = await image.toEncodedImageData('jpg', 90)
const sameImageCopied = await Images.loadFromEncodedImageData(imageData)An Image can be resized entirely in-memory, without ever writing to- or reading from- a file:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const smaller = await webImage.resizeAsync(200, 200)An Image can be cropped entirely in-memory, without ever writing to- or reading from- a file:
const webImage = await WebImages.loadFromURLAsync('https://picsum.photos/seed/123/400')
const smaller = await webImage.cropAsync(100, 100, 50, 50)An in-memory Image object can also be written/saved to a file:
const image = ...
const path = await image.saveToTemporaryFileAsync('jpg', 90)Images can be compressed using the jpg container format - either in-memory or when writing to a file:
const image = ...
const path = await image.saveToTemporaryFileAsync('jpg', 50) // 50% compression
const compressed = await image.toEncodedImageData('jpg', 50) // 50% compressionNitroImage supports HEIC/HEIF format if the host OS natively supports it.
| iOS | Android | |
|---|---|---|
| Loading HEIC | ✅ | ✅ (>= SDK 28) |
| Writing HEIC | ✅ (>= iOS 17) | ❌ |
You can check whether your OS supports HEIC via NitroImage:
import { supportsHeicWriting } from 'react-native-nitro-modules'
const image = ...
const format = supportsHeicWriting ? 'heic' : 'jpg'
const path = await image.saveToTemporaryFileAsync(format, 100)The useImage() hook asynchronously loads an Image from the given source and returns it as a React state:
function App() {
const image = useImage({ filePath: '/tmp/image.jpg' })
return …
}The useImageLoader() hook creates an asynchronous ImageLoader which can be passed to a <NitroImage /> view to defer image loading:
function App() {
const loader = useImageLoader({ filePath: '/tmp/image.jpg' })
return (
<NitroImage
image={loader}
style={{ width: 400, height: 400 }}
/>
)
}The <NitroImage /> view is a React Native view that allows you to render Image - either asynchronously (by wrapping ImageLoaders), or synchronously (by passing Image instances directly):
function App() {
return (
<NitroImage
image={{ filePath: '/tmp/image.jpg' }}
style={{ width: 400, height: 400 }}
/>
)
}The <NativeNitroImage /> view is the actual native Nitro View component for rendering an Image instance. It is recommended to use abstractions like <NitroImage /> instead of the actual native component. However if you need to use the native component instead, it is still exposed:
function App() {
const image = …
return (
<NativeNitroImage
image={image}
style={{ width: 400, height: 400 }}
/>
)
}To achieve a dynamic width or height calculation, you can use the image's dimensions:
function App() {
const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
const aspect = (image?.width ?? 1) / (image?.height ?? 1)
return (
<NitroImage
image={image}
style={{ width: '100%', aspectRatio: aspect }}
/>
)
}This will now resize the height dimension to match the same aspect ratio as the image - in this case it will be 1:1 since the image is 400x400.
If the image is 400x200, the height of the view will be half of the width of the view, i.e. a 0.5 aspect ratio.
A ThumbHash is a short binary (or base64 string) representation of a blurry image.
Since it is a very small buffer (or base64 string), it can be added to a payload (like a user object in your database) to immediately display an image placeholder while the actual image loads.
Usage Example
For example, your users database could have a users.profile_picture_url field which you use to asynchronously load the web Image, and a users.profile_picture_thumbhash field which contains the ThumbHash buffer (or base64 string) which you can display on-device immediately.
usersusers.profile_picture_url: Load asynchronouslyusers.profile_picture_thumbhash: Decode & Display immediately
Everytime you upload a new profile picture for the user, you should encode the image to a new ThumbHash again and update the users.profile_picture_thumbhash field. This should ideally happen on your backend, but can also be performed on-device if needed.
NitroImage supports conversion from- and to- ThumbHash representations out of the box.
For performance reasons, a ThumbHash is represented as an ArrayBuffer.
const thumbHash = ...from server
const image = Images.loadFromThumbHash(thumbHash)
const thumbHashAgain = image.toThumbHash()If your ThumbHash is a string, convert it to an ArrayBuffer first, since this is more efficient:
const thumbHashBase64 = ...from server
const thumbHashArrayBuffer = thumbHashFromBase64String(thumbHashBase64)
const thumbHashBase64Again = thumbHashToBase64String(thumbHashArrayBuffer)Since ThumbHash decoding or encoding can be a slow process, you should consider using the async methods instead:
const thumbHash = ...from server
const image = await Images.loadFromThumbHashAsync(thumbHash)
const thumbHashAgain = await image.toThumbHash()To use the native Image type in your library (e.g. in a Camera library), you need to follow these steps:
- Add the dependency on
react-native-nitro-image- JS: Add
react-native-nitro-imagetopeerDependenciesanddevDependencies - Android: Add
:react-native-nitro-imageto yourbuild.gradle'sdependencies, andreact-native-nitro-image::NitroImageto your CMake's dependencies (it's a prefab) - iOS: Add
NitroImageto your*.podspec's dependencies
- JS: Add
- In your Nitro specs (
*.nitro.ts), just importImagefrom'react-native-nitro-image'and use it as a type - In your native implementation, you can either;
- Implement
HybridImageSpec,HybridImageLoaderSpecorHybridImageViewSpecwith your custom implementation, e.g. to create aImageimplementation that doesn't useUIImagebut instead usesCGImage, or anAVPhoto - Use the
HybridImageSpec,HybridImageLoaderSpecorHybridImageViewSpectypes. You can either use them abstract (with all the methods that are also exposed to JS), or by downcasting them to a specific type - all of them follow a protocol likeNativeImage:class HybridCustom: HybridCustomSpec { func doSomething(image: any HybridImageSpec) throws { guard let image = image as? NativeImage else { return } let uiImage = image.uiImage // ... } }
- Implement
- Done! 🎉 Now you can benefit from a common, shared
Imagetype - e.g. your Camera library can directly return anImageinstance intakePhoto(), which can be instantly rendered using<NitroImage />- no more file I/O!