From 0c0d9b1aa43299fdebab4cbd418ff09715db61dd Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 11 Mar 2026 15:17:56 +0100 Subject: [PATCH] perf: Do some improvements --- example/src/NitroImageTab.tsx | 2 + .../nitro/image/HybridImageLoadTicket.kt | 6 ++ .../margelo/nitro/image/HybridImageLoader.kt | 3 +- .../margelo/nitro/image/HybridImageView.kt | 56 +++++++++++++++++-- .../nitro/web/image/HybridWebImageLoader.kt | 11 ++-- 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoadTicket.kt diff --git a/example/src/NitroImageTab.tsx b/example/src/NitroImageTab.tsx index 850af923..ad4f40ca 100644 --- a/example/src/NitroImageTab.tsx +++ b/example/src/NitroImageTab.tsx @@ -13,9 +13,11 @@ export function NitroImageTab() { numColumns={4} windowSize={3} data={imageURLs} + keyExtractor={(url) => url} renderItem={({ item: url }) => ( diff --git a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoadTicket.kt b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoadTicket.kt new file mode 100644 index 00000000..fc4d9657 --- /dev/null +++ b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoadTicket.kt @@ -0,0 +1,6 @@ +package com.margelo.nitro.image + +data class HybridImageLoadTicket( + val generation: Long, + val bindingKey: String +) diff --git a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoader.kt b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoader.kt index 606e624d..5be40dcb 100644 --- a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoader.kt +++ b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageLoader.kt @@ -33,11 +33,12 @@ class HybridImageLoader( override fun requestImage(forView: HybridNitroImageViewSpec) { val view = forView as? HybridImageView ?: return + val ticket = view.beginLoad(this) ?: return loadImage().then { maybeImage -> val image = maybeImage as? HybridImage ?: return@then uiScope.launch { - view.imageView.setImageBitmap(image.bitmap) + view.applyLoadedBitmap(ticket, image.bitmap) } } } diff --git a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageView.kt b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageView.kt index 672c3e2c..629771e2 100644 --- a/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageView.kt +++ b/packages/react-native-nitro-image/android/src/main/java/com/margelo/nitro/image/HybridImageView.kt @@ -1,6 +1,7 @@ package com.margelo.nitro.image import android.content.Context +import android.graphics.Bitmap import android.util.Log import android.view.View import android.widget.ImageView @@ -20,8 +21,12 @@ class HybridImageView(context: Context): HybridNitroImageViewSpec(), RecyclableV } private val uiScope = CoroutineScope(Dispatchers.Main.immediate) private var resetImageBeforeLoad = false + private var isAttached = false + private var activeLoadTicket: HybridImageLoadTicket? = null + private var nextLoadGeneration = 0L val imageView = CustomImageView(context) { visible -> + isAttached = visible if (visible) onAppear() else onDisappear() } @@ -51,7 +56,7 @@ class HybridImageView(context: Context): HybridNitroImageViewSpec(), RecyclableV override fun prepareForRecycle() { onDisappear() - imageView.setImageBitmap(null) + imageView.setImageDrawable(null) } private fun updateResizeMode() { @@ -67,7 +72,7 @@ class HybridImageView(context: Context): HybridNitroImageViewSpec(), RecyclableV private fun updateImage() { image?.match( { actualImage: HybridImageSpec -> - // Image + clearActiveRequest() if (actualImage is HybridImage) { imageView.setImageBitmap(actualImage.bitmap) } else { @@ -75,8 +80,10 @@ class HybridImageView(context: Context): HybridNitroImageViewSpec(), RecyclableV } }, { _: HybridImageLoaderSpec -> - // ImageLoader - onAppear() + // Defer loader work until the view is attached to avoid offscreen requests. + if (isAttached) { + onAppear() + } } ) } @@ -96,10 +103,51 @@ class HybridImageView(context: Context): HybridNitroImageViewSpec(), RecyclableV private fun onDisappear() { val imageLoader = image?.asSecondOrNull() ?: return + clearActiveRequest() try { imageLoader.dropImage(this) } catch (e: Throwable) { Log.e(TAG, "Failed to drop Image!", e) } } + + fun beginLoad(imageLoader: HybridImageLoaderSpec): HybridImageLoadTicket? { + if (!isAttached) { + return null + } + + val bindingKey = bindingKeyFor(imageLoader) + if (activeLoadTicket?.bindingKey == bindingKey) { + return null + } + + val ticket = HybridImageLoadTicket(++nextLoadGeneration, bindingKey) + activeLoadTicket = ticket + return ticket + } + + fun applyLoadedBitmap( + ticket: HybridImageLoadTicket, + bitmap: Bitmap + ): Boolean { + if (!isAttached) { + return false + } + + if (activeLoadTicket != ticket) { + return false + } + + // Ignore async completions that belong to an older binding or a detached view. + imageView.setImageBitmap(bitmap) + return true + } + + private fun clearActiveRequest() { + activeLoadTicket = null + } + + private fun bindingKeyFor(imageLoader: HybridImageLoaderSpec): String { + return "${System.identityHashCode(imageLoader)}:${recyclingKey ?: "none"}" + } } diff --git a/packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/HybridWebImageLoader.kt b/packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/HybridWebImageLoader.kt index d4fd6b40..65d7f0a2 100644 --- a/packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/HybridWebImageLoader.kt +++ b/packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/HybridWebImageLoader.kt @@ -1,10 +1,11 @@ package com.margelo.nitro.web.image import android.content.Context -import android.widget.ImageView import coil3.ImageLoader +import coil3.dispose import coil3.load import com.margelo.nitro.core.Promise +import com.margelo.nitro.image.HybridImageView import com.margelo.nitro.image.HybridImageSpec import com.margelo.nitro.image.HybridImageLoaderSpec import com.margelo.nitro.image.HybridNitroImageViewSpec @@ -20,14 +21,16 @@ class HybridWebImageLoader(private val imageLoader: ImageLoader, } override fun requestImage(forView: HybridNitroImageViewSpec) { - val imageView = forView.view as? ImageView ?: return + val view = forView as? HybridImageView ?: return + if (view.beginLoad(this) == null) return - imageView.load(url, imageLoader) { + view.imageView.load(url, imageLoader) { this.applyOptions(options) } } override fun dropImage(forView: HybridNitroImageViewSpec) { - // Coil automatically handles recycling here - I _think_. + val view = forView as? HybridImageView ?: return + view.imageView.dispose() } }