react-native-nitro-image-pipeline 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -259,11 +259,17 @@ omitted.
259
259
 
260
260
  ### `preLoadImage(url)`
261
261
 
262
- Prefetches a single image into the cache. Returns `Promise<void>`.
262
+ Prefetches a single image into the **disk cache**, without decoding it. Returns `Promise<void>`.
263
+
264
+ Prefetching only pays the network and disk I/O cost up front — no bitmap is decoded or held in
265
+ memory, so prefetching a long list of URLs doesn't balloon RAM. The image is decoded (at the
266
+ `resize` target size, when one is given) the first time `loadImage`/`useImage`/`<PipelineImage>`
267
+ actually displays it.
263
268
 
264
269
  ### `preLoadImages(urls)`
265
270
 
266
- Prefetches multiple images into the cache. Returns `Promise<void>`.
271
+ Prefetches multiple images into the disk cache same behavior as `preLoadImage`, for a batch.
272
+ Returns `Promise<void>`.
267
273
 
268
274
  ### `gaussianBlur(image, radius)`
269
275
 
@@ -304,6 +310,23 @@ images keep their borders instead of fading out.
304
310
 
305
311
  Removes all cached images from memory and disk. Returns `Promise<void>` that resolves once both caches are cleared.
306
312
 
313
+ ## Memory usage
314
+
315
+ The pipeline is set up so RAM scales with what you display, not with what you download:
316
+
317
+ - **Pass `resize` (or just use `<PipelineImage>`, which derives it from layout).** With a target
318
+ size known, both platforms decode the source *near that size* instead of at full resolution —
319
+ iOS via a downsampled thumbnail decode, Android via Coil's subsampling. Without `resize`, a
320
+ 48 MP photo decompresses to ~190 MB of bitmap no matter how small you display it.
321
+ - **Prefetching stores bytes, not bitmaps.** `preLoadImage(s)` writes the download to the disk
322
+ cache and skips decoding entirely.
323
+ - **The in-memory cache is capped** (128 MB on iOS, 25% of the app's memory class on Android),
324
+ holds decoded bitmaps for instant re-display, and evicts least-recently-used entries — also in
325
+ response to memory warnings and backgrounding. Memory profilers attribute this cache to the app;
326
+ a plateau at the cap is expected and evictable, not a leak. Use `cache: 'disk'` or
327
+ `cache: 'none'` on images you know you won't show again soon, and `clearCache()` to drop
328
+ everything.
329
+
307
330
  ## Upgrading from 0.3.x
308
331
 
309
332
  `blur` and `gaussianBlur(image, radius)` changed meaning in 1.0. They used to hand the number
@@ -3,8 +3,11 @@ package com.margelo.nitro.nitroimagepipeline
3
3
  import android.content.Context
4
4
  import androidx.core.graphics.drawable.toBitmap
5
5
  import coil3.BitmapImage
6
+ import coil3.ColorImage
6
7
  import coil3.DrawableImage
7
8
  import coil3.ImageLoader
9
+ import coil3.decode.DecodeResult
10
+ import coil3.decode.Decoder
8
11
  import coil3.disk.DiskCache
9
12
  import coil3.memory.MemoryCache
10
13
  import coil3.network.okhttp.OkHttpNetworkFetcherFactory
@@ -117,21 +120,25 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
117
120
  }
118
121
  }
119
122
 
123
+ // Preloading warms the disk cache only: the memory cache is skipped and the
124
+ // decode step is replaced with a no-op (Coil's documented preload pattern),
125
+ // so prefetching N URLs costs network + disk I/O instead of N full-resolution
126
+ // bitmaps on the heap. The image is decoded — subsampled to the requested
127
+ // size — only when a loadImage actually displays it.
128
+ private fun preloadRequest(url: String): ImageRequest =
129
+ ImageRequest.Builder(context)
130
+ .data(url)
131
+ .memoryCachePolicy(CachePolicy.DISABLED)
132
+ .decoderFactory { _, _, _ -> Decoder { DecodeResult(ColorImage(), false) } }
133
+ .build()
134
+
120
135
  override fun preLoadImage(url: String): Promise<Unit> = Promise.async {
121
- val request = ImageRequest.Builder(context).data(url).build()
122
- imageLoader.execute(request)
136
+ imageLoader.execute(preloadRequest(url))
123
137
  }
124
138
 
125
139
  override fun preLoadImages(urls: Array<String>): Promise<Unit> = Promise.async {
126
140
  coroutineScope {
127
- urls
128
- .map { url ->
129
- async {
130
- val request = ImageRequest.Builder(context).data(url).build()
131
- imageLoader.execute(request)
132
- }
133
- }
134
- .awaitAll()
141
+ urls.map { url -> async { imageLoader.execute(preloadRequest(url)) } }.awaitAll()
135
142
  }
136
143
  Unit
137
144
  }
@@ -95,10 +95,28 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
95
95
  // the original download, so they survive memory eviction and
96
96
  // restarts without dropping the original for other variants.
97
97
  configuration.dataCachePolicy = .storeAll
98
+ // A private, capped memory cache. The default (ImageCache.shared)
99
+ // sizes itself at 15% of physical RAM — up to 768 MB of decoded
100
+ // bitmaps on modern devices before anything is evicted, which shows
101
+ // up as "high RAM usage" even though it is all evictable cache.
102
+ // 128 MB still holds ~10 full-screen bitmaps or hundreds of list
103
+ // thumbnails, and the LRU cache keeps trimming itself on memory
104
+ // warnings and when the app enters the background.
105
+ configuration.imageCache = ImageCache(
106
+ costLimit: min(ImageCache.defaultCostLimit, 128 * 1024 * 1024)
107
+ )
98
108
  return ImagePipeline(configuration: configuration)
99
109
  }()
100
110
 
101
- private static let sharedPrefetcher = ImagePrefetcher(pipeline: sharedPipeline)
111
+ // `.diskCache` stores the downloaded data without decoding it, so
112
+ // prefetching N URLs costs network + disk I/O instead of N decoded
113
+ // full-resolution bitmaps parked in the memory cache (the default
114
+ // `.memoryCache` destination). The image is decoded — at the requested
115
+ // target size — only when a loadImage actually displays it.
116
+ private static let sharedPrefetcher = ImagePrefetcher(
117
+ pipeline: sharedPipeline,
118
+ destination: .diskCache
119
+ )
102
120
 
103
121
  private var pipeline: ImagePipeline { Self.sharedPipeline }
104
122
  private var prefetcher: ImagePrefetcher { Self.sharedPrefetcher }
@@ -133,13 +151,21 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
133
151
  }
134
152
  }
135
153
 
154
+ /// The target size in pixels, or `nil` when no (valid) resize was requested.
155
+ private static func resizeSize(for options: Options?) -> CGSize? {
156
+ guard let resize = options?.resize, resize.width > 0, resize.height > 0 else {
157
+ return nil
158
+ }
159
+ return CGSize(width: resize.width, height: resize.height)
160
+ }
161
+
136
162
  private static func processors(for options: Options?) -> [any ImageProcessing] {
137
163
  var processors: [any ImageProcessing] = []
138
164
  // Resize first: blur sigma and corner radii are defined in pixels
139
165
  // of the bitmap they run on, so they must see the final size.
140
- if let resize = options?.resize, resize.width > 0, resize.height > 0 {
166
+ if let size = resizeSize(for: options) {
141
167
  processors.append(ImageProcessors.Resize(
142
- size: CGSize(width: resize.width, height: resize.height),
168
+ size: size,
143
169
  unit: .pixels,
144
170
  contentMode: .aspectFill,
145
171
  crop: true,
@@ -161,11 +187,24 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
161
187
  throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
162
188
  }
163
189
 
164
- let imgRequest = ImageRequest(
190
+ var imgRequest = ImageRequest(
165
191
  url: imageUrl,
166
192
  processors: Self.processors(for: options),
167
193
  options: Self.cacheOptions(for: options?.cache)
168
194
  )
195
+ // With a target size known, decode near it (aspect-fill, so the
196
+ // decoded image always covers the target) instead of at full
197
+ // resolution — a 48 MP photo displayed as a 300 pt card would
198
+ // otherwise decompress to ~190 MB before Resize shrinks it.
199
+ // Matches Android, where the request's size() drives subsampling;
200
+ // the exact size and crop still come from the Resize processor.
201
+ if let size = Self.resizeSize(for: options) {
202
+ imgRequest.thumbnail = ImageRequest.ThumbnailOptions(
203
+ size: size,
204
+ unit: .pixels,
205
+ contentMode: .aspectFill
206
+ )
207
+ }
169
208
 
170
209
  let image = try await self.pipeline.image(for: imgRequest)
171
210
  return HybridImage(uiImage: image)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-image-pipeline",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "High-performance image loading, caching, and processing for React Native, built with Nitro Modules",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",