From a67dce588ad3b79767a2be9818848fa15f51ec44 Mon Sep 17 00:00:00 2001 From: Dave Mkpa-Eke Date: Sat, 23 May 2026 00:40:35 +0100 Subject: [PATCH 1/5] feat: priority caching --- bun.lock | 19 +-- example/package.json | 2 +- example/src/App.tsx | 2 + example/src/CachePriorityTab.tsx | 101 ++++++++++++ .../margelo/nitro/image/HybridImageView.kt | 2 + .../ios/HybridImageView.swift | 1 + .../android/c++/JHybridNitroImageViewSpec.cpp | 9 ++ .../android/c++/JHybridNitroImageViewSpec.hpp | 2 + .../JHybridNitroImageViewStateUpdater.cpp | 4 + .../kotlin/com/margelo/nitro/image/Color.kt | 2 +- .../margelo/nitro/image/EncodedImageData.kt | 2 +- .../nitro/image/HybridNitroImageViewSpec.kt | 6 + .../com/margelo/nitro/image/RawPixelData.kt | 2 +- .../ios/c++/HybridNitroImageViewSpecSwift.hpp | 7 + .../views/HybridNitroImageViewComponent.mm | 5 + .../ios/swift/HybridNitroImageViewSpec.swift | 1 + .../swift/HybridNitroImageViewSpec_cxx.swift | 24 +++ .../shared/c++/HybridNitroImageViewSpec.cpp | 2 + .../shared/c++/HybridNitroImageViewSpec.hpp | 2 + .../views/HybridNitroImageViewComponent.cpp | 11 ++ .../views/HybridNitroImageViewComponent.hpp | 1 + .../shared/json/NitroImageViewConfig.json | 1 + .../react-native-nitro-image/package.json | 4 +- .../src/specs/ImageView.nitro.ts | 14 ++ .../android/build.gradle | 5 + .../nitro/web/image/HybridWebImageFactory.kt | 17 +- .../nitro/web/image/HybridWebImageLoader.kt | 3 + .../web/image/cache/PriorityMemoryCache.kt | 149 ++++++++++++++++++ .../nitro/web/image/cache/WeakImageCache.kt | 119 ++++++++++++++ .../ios/Cache/OrderedKeyMap.swift | 73 +++++++++ .../ios/Cache/PriorityMemoryCache.swift | 139 ++++++++++++++++ .../ios/HybridWebImageFactory.swift | 17 ++ .../ios/HybridWebImageLoader.swift | 11 +- .../c++/JHybridWebImageFactorySpec.cpp | 10 +- .../c++/JHybridWebImageFactorySpec.hpp | 3 +- .../web/image/HybridWebImageFactorySpec.kt | 6 +- .../c++/HybridWebImageFactorySpecSwift.hpp | 7 +- .../ios/swift/HybridWebImageFactorySpec.swift | 2 +- .../swift/HybridWebImageFactorySpec_cxx.swift | 11 +- .../shared/c++/HybridWebImageFactorySpec.cpp | 2 + .../shared/c++/HybridWebImageFactorySpec.hpp | 3 +- .../react-native-nitro-web-image/package.json | 4 +- .../src/specs/WebImageFactory.nitro.ts | 12 ++ 43 files changed, 793 insertions(+), 26 deletions(-) create mode 100644 example/src/CachePriorityTab.tsx create mode 100644 packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/cache/PriorityMemoryCache.kt create mode 100644 packages/react-native-nitro-web-image/android/src/main/java/com/margelo/nitro/web/image/cache/WeakImageCache.kt create mode 100644 packages/react-native-nitro-web-image/ios/Cache/OrderedKeyMap.swift create mode 100644 packages/react-native-nitro-web-image/ios/Cache/PriorityMemoryCache.swift diff --git a/bun.lock b/bun.lock index ba9fded4..1ce4eb7e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "react-native-nitro-image-monorepo", @@ -29,7 +30,7 @@ "react-native-fast-image": "^8.6.3", "react-native-harness": "^1.0.0-alpha.25", "react-native-nitro-image": "workspace:*", - "react-native-nitro-modules": "0.35.5", + "react-native-nitro-modules": "0.35.7", "react-native-nitro-web-image": "workspace:*", "react-native-safe-area-context": "^5.6.0", "react-native-screens": "^4.14.1", @@ -56,10 +57,10 @@ "version": "0.14.0", "devDependencies": { "@types/react": "^19.0.6", - "nitrogen": "0.35.5", + "nitrogen": "0.35.7", "react": "19.1.0", "react-native": "0.81.0", - "react-native-nitro-modules": "0.35.5", + "react-native-nitro-modules": "0.35.7", "typescript": "5.8.3", }, "peerDependencies": { @@ -73,10 +74,10 @@ "version": "0.14.0", "devDependencies": { "@types/react": "^19.0.6", - "nitrogen": "0.35.5", + "nitrogen": "0.35.7", "react": "19.1.0", "react-native": "0.81.0", - "react-native-nitro-modules": "0.35.5", + "react-native-nitro-modules": "0.35.7", "typescript": "5.8.3", }, "peerDependencies": { @@ -620,7 +621,7 @@ "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + "@ts-morph/common": ["@ts-morph/common@0.29.0", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg=="], "@tsconfig/react-native": ["@tsconfig/react-native@2.0.3", "", {}, "sha512-jE58snEKBd9DXfyR4+ssZmYJ/W2mOSnNrvljR0aLyQJL9JKX6vlWELHkRjb3HBbcM9Uy0hZGijXbqEAjOERW2A=="], @@ -1456,7 +1457,7 @@ "new-github-release-url": ["new-github-release-url@2.0.0", "", { "dependencies": { "type-fest": "^2.5.1" } }, "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ=="], - "nitrogen": ["nitrogen@0.35.5", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.35.5", "ts-morph": "^27.0.0", "yargs": "^18.0.0", "zod": "^4.0.5" }, "bin": { "nitrogen": "lib/index.js" } }, "sha512-Ofl4aTW2rd44+hBcUwLXu01ViHikn2SDI7zOYc+6NcKSpnhoj42k1FwyvRj1QZPDMUT64Bwlb0DYw7Hrfd3BfQ=="], + "nitrogen": ["nitrogen@0.35.7", "", { "dependencies": { "chalk": "^5.3.0", "react-native-nitro-modules": "^0.35.7", "ts-morph": "^28.0.0", "yargs": "^18.0.0", "zod": "^4.0.5" }, "bin": { "nitrogen": "lib/index.js" } }, "sha512-+uXdeK1NhUfXW562qCAnN/mMzVuEUw1K58V1oelhBBQMBTZlTvfO4yTbCFX76XjOeRnbMrPosqIGSGQh3hz1MQ=="], "nocache": ["nocache@3.0.4", "", {}, "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw=="], @@ -1622,7 +1623,7 @@ "react-native-nitro-image": ["react-native-nitro-image@workspace:packages/react-native-nitro-image"], - "react-native-nitro-modules": ["react-native-nitro-modules@0.35.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-aa03UzC5dLg5qFfyBkVK+JGSwHTjmK7jUZzyRz11r1Yk9C/nJTFe59EeHPxxNNTagkiwQTM6p3sySgD/TDRC7Q=="], + "react-native-nitro-modules": ["react-native-nitro-modules@0.35.7", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-3EiU27EmnxTlD3pAXR2OBWeDg7+9tdjKrNQOPX08rFX5hJPNIT/h1i2PjvTUieOypCGTEi9nW/SNBtK3F+4HYg=="], "react-native-nitro-web-image": ["react-native-nitro-web-image@workspace:packages/react-native-nitro-web-image"], @@ -1808,7 +1809,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + "ts-morph": ["ts-morph@28.0.0", "", { "dependencies": { "@ts-morph/common": "~0.29.0", "code-block-writer": "^13.0.3" } }, "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/example/package.json b/example/package.json index d47826e0..c20bb8cd 100644 --- a/example/package.json +++ b/example/package.json @@ -20,7 +20,7 @@ "react-native-harness": "^1.0.0-alpha.25", "react-native-nitro-image": "workspace:*", "react-native-nitro-web-image": "workspace:*", - "react-native-nitro-modules": "0.35.5", + "react-native-nitro-modules": "0.35.7", "react-native-safe-area-context": "^5.6.0", "react-native-screens": "^4.14.1" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index 2dff6a2c..bc6270ce 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,6 +7,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import { createStaticNavigation } from '@react-navigation/native' +import { CachePriorityTab } from './CachePriorityTab' import { EmptyTab } from './EmptyTab' import { FastImageTab } from './FastImageTab' import { NitroImageTab } from './NitroImageTab' @@ -17,6 +18,7 @@ const Tabs = createBottomTabNavigator({ Empty: EmptyTab, FastImage: FastImageTab, NitroImage: NitroImageTab, + CachePriority: CachePriorityTab, }, }) const Navigation = createStaticNavigation(Tabs) diff --git a/example/src/CachePriorityTab.tsx b/example/src/CachePriorityTab.tsx new file mode 100644 index 00000000..07fb5037 --- /dev/null +++ b/example/src/CachePriorityTab.tsx @@ -0,0 +1,101 @@ +import { useCallback, useMemo, useState } from 'react' +import { Button, FlatList, Platform, StyleSheet, Text, View } from 'react-native' +import { NitroImage } from 'react-native-nitro-image' +import { WebImages } from 'react-native-nitro-web-image' +import { createImageURLs } from './createImageURLs' + +// Baseline run: tight cap + no priorities anywhere. 3 MB sits just above the +// 800×800 hero (~2.56 MB decoded RGBA) so it can land in cache, then the +// first thumbnail load should LRU-evict it. +const DEMO_CACHE_BYTES = 3 * 1024 * 1024 + +console.log(`[CachePriorityTab] platform: ${Platform.OS}`) +console.log(`[CachePriorityTab] maxMemoryBytes before: ${WebImages.maxMemoryBytes}`) +WebImages.maxMemoryBytes = DEMO_CACHE_BYTES +console.log(`[CachePriorityTab] maxMemoryBytes after: ${WebImages.maxMemoryBytes}`) + +/** + * Demonstrates cachePriority. The hero (priority 2) should survive cache + * pressure while thumbnails (priority 0) churn. With the configured cap, + * scrolling many thumbs evicts thumbs first; tapping "Reload hero" should + * give an instant cache hit, while thumbnails that scrolled off may need + * to be refetched. + */ +export function CachePriorityTab() { + const heroURL = useMemo(() => `https://picsum.photos/seed/hero/800`, []) + const thumbnailURLs = useMemo(() => createImageURLs(200), []) + const [heroNonce, setHeroNonce] = useState(0) + + // Pre-build the {url} objects once so each row's `image` prop has a stable + // reference across re-renders — otherwise an inline `{ url }` literal would + // create a new object every render and re-trigger the native `image` setter. + const thumbnailSources = useMemo( + () => thumbnailURLs.map((url) => ({ url })), + [thumbnailURLs], + ) + + // Stable identity per hero remount; tied to the nonce so the URL identity + // changes only when the user taps "Reload hero". + const heroSource = useMemo(() => ({ url: heroURL }), [heroURL, heroNonce]) + + const renderItem = useCallback( + ({ item: source }: { item: { url: string } }) => ( + + ), + [], + ) + + return ( + + Hero (no cachePriority) + + + Remounted: {heroNonce} • check logcat: 1 SET, {heroNonce} HITs +