npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kayzmann/react-native-image-cache

v1.1.0

Published

Production-ready image caching for React Native with LRU eviction, S3 presigned URL support, and Instagram-like offline experience

Readme

@kayzmann/react-native-image-cache

Production-ready image caching for React Native with LRU eviction, S3 presigned URL support, and Instagram-like offline experience.

npm version License: MIT

🎯 The Problem

You're building a React Native app that displays images from Amazon S3 (or any cloud storage with presigned URLs). Your backend generates presigned URLs that expire after 15 minutes.

Here's what happens with traditional image caching solutions:

// ❌ Traditional approach (BROKEN)
<Image source={{ uri: presignedUrl }} />
// Works for 15 minutes... then 403 Forbidden!
// Image reloads every time the URL refreshes
// No offline support
// Wastes bandwidth re-downloading the same image

The Pain Points

  1. Presigned URLs Expire - After 15 minutes, the URL returns 403 Forbidden
  2. Constant Re-downloads - Same image downloaded repeatedly when URL refreshes
  3. No Offline Experience - Images disappear when offline (unlike Instagram)
  4. High S3 Costs - Unnecessary bandwidth charges from re-downloads
  5. Poor UX - Users see loading states for images they've already viewed

Why Existing Packages Don't Solve This

We researched popular packages:

  • react-native-fast-image - Struggles with S3 presigned URLs, reloads when URL changes
  • @georstat/react-native-image-cache - Uses lastModified (time-based), NOT true LRU (Least Recently Used)
  • react-native-cached-image - Older, basic caching without LRU eviction

None of them cache the image BYTES with a stable identifier separate from the expiring URL.

✨ The Solution

This package solves the problem by:

1. Caching Image Bytes, Not URLs

// ✅ Our approach (WORKS)
<CachedImage
  url={trade.serviceImageUrl}      // Pre-signed URL (expires every 15 min)
  cacheKey={trade.serviceImage}    // S3 key (NEVER changes)
/>

// How it works:
// 1. Downloads image BYTES from presigned URL
// 2. Saves to disk using STABLE cacheKey: "trade_photo:9:abc123"
// 3. Next time (even with new URL): Uses cached bytes
// 4. Works offline! Shows cached image even when network is down

2. True LRU Eviction Policy

Unlike other packages that delete the "oldest file", we track when each image was last ACCESSED:

// When you view an image → Updates lastAccessed timestamp
// When cache exceeds 100MB → Deletes LEAST RECENTLY ACCESSED images
// Frequently viewed images stay cached longer

3. Instagram-like Offline Experience

// User scrolls through feed → Images cache automatically
// User goes offline → Cached images still display
// User comes back online → New images download in background
// Just like Instagram! 📸

🚀 Quick Start

Installation

npm install @kayzmann/react-native-image-cache
# or
yarn add @kayzmann/react-native-image-cache

Peer Dependencies

# If using Expo (recommended):
npx expo install expo-file-system expo-image expo-image-manipulator

# If using bare React Native:
npm install expo-file-system expo-image expo-image-manipulator

Basic Usage

import { CachedImage } from '@kayzmann/react-native-image-cache';

// Your API returns:
const trade = {
  serviceImage: "trade_photo:9:aad6a00c7dfe5175",  // ✅ STABLE - never changes
  serviceImageUrl: "https://s3...?X-Amz-Expires=900"  // ⏰ EXPIRES in 15 min
};

// Use it:
<CachedImage
  url={trade.serviceImageUrl}      // Pre-signed URL
  cacheKey={trade.serviceImage}    // Stable S3 key
  style={{ width: 100, height: 100 }}
  resizeMode="cover"
/>

// First render: Downloads from URL, caches as "trade_photo_9_aad6a00c7dfe5175.jpg"
// 20 minutes later (URL expired): Uses cached file, ignores expired URL ✨
// Offline: Shows cached image! 🎉

📖 Usage Examples

With Presets (Recommended)

Choose a preset that matches your app's needs:

import { imageCacheService, defaultPreset } from '@kayzmann/react-native-image-cache';

// In your App.tsx (before any images load):
imageCacheService.updateConfig(defaultPreset);

// Or use a different preset:
import { instagramLikePreset } from '@kayzmann/react-native-image-cache';
imageCacheService.updateConfig(instagramLikePreset);

Available Presets:

| Preset | Cache Size | Age | Downloads | Compression | Use Case | |--------|-----------|-----|-----------|-------------|----------| | defaultPreset | 100 MB | 30 days | 3 | No | Balanced for most apps | | aggressivePreset | 50 MB | 7 days | 2 | Yes (70%) | Limited storage | | conservativePreset | 200 MB | 90 days | 5 | No | Plenty of storage, minimize S3 costs | | minimalPreset | 25 MB | 3 days | 2 | Yes (75%) | Very limited storage | | instagramLikePreset | 300 MB | 60 days | 6 | Yes (85%) | Social media apps | | ecommercePreset | 150 MB | 45 days | 4 | No | Marketplace/product images |

Note on Compression: When compression is enabled, the library uses expo-image-manipulator to compress images using lossy JPEG compression. The percentages shown (70%, 75%, 85%) represent quality settings that balance file size reduction with visual fidelity. Higher values (like 85%) maintain better image quality but result in larger files, while lower values (like 70%) produce smaller files with more visible compression artifacts. This is ideal for reducing cache size when storage is limited.

Custom Configuration

import { imageCacheService } from '@kayzmann/react-native-image-cache';

imageCacheService.updateConfig({
  maxCacheSizeMB: 100,          // Max cache size (MB)
  maxCacheAgeDays: 30,          // Auto-delete images older than 30 days
  maxConcurrentDownloads: 3,    // Prevent memory issues
  enableCompression: true,      // Enable JPEG compression (default: false)
  compressionQuality: 0.8,      // Compression quality 0.0-1.0 (default: 0.85)
  enableDebugLogging: __DEV__,  // Log cache operations
  onCacheHit: (key) => console.log('Cache hit:', key),
  onCacheMiss: (key) => console.log('Cache miss:', key),
  onEviction: (key, size) => console.log('Evicted:', key, size),
});

Pre-caching Images

Perfect for when you fetch a list from your API:

import { usePreCacheImages } from '@kayzmann/react-native-image-cache';

function TradeList() {
  const { data: trades } = useQuery('trades', fetchTrades);

  // Pre-cache all images in the background
  usePreCacheImages(
    trades.map(trade => ({
      url: trade.serviceImageUrl,
      cacheKey: trade.serviceImage,
    }))
  );

  return (
    <FlatList
      data={trades}
      renderItem={({ item }) => (
        <CachedImage
          url={item.serviceImageUrl}
          cacheKey={item.serviceImage}
          style={{ width: 100, height: 100 }}
        />
      )}
    />
  );
}

Using the Hook

For more control:

import { useCachedImage } from '@kayzmann/react-native-image-cache';

function MyComponent() {
  const { source, isCached, isLoading, refresh } = useCachedImage({
    url: trade.serviceImageUrl,
    cacheKey: trade.serviceImage,
    fallbackUrl: DEFAULT_IMAGE,
  });

  return (
    <View>
      <Image source={{ uri: source }} style={{ width: 100, height: 100 }} />
      {isCached && <Text>✅ Cached</Text>}
      {isLoading && <ActivityIndicator />}
      <Button title="Refresh" onPress={refresh} />
    </View>
  );
}

Manual Cache Operations

import { imageCacheService } from '@kayzmann/react-native-image-cache';

// Get cache statistics
const stats = await imageCacheService.getCacheStats();
console.log(`${stats.totalImages} images, ${stats.totalSizeMB.toFixed(2)} MB`);

// Clear cache (e.g., on logout)
await imageCacheService.clearImageCache();

// Delete specific image
await imageCacheService.deleteCachedImage('trade_photo:9:abc123');

// Get cache size
const sizeInBytes = await imageCacheService.getCacheSize();

🏗️ How It Works

Architecture

┌─────────────────────────────────────────────────────┐
│          ImageCache Service (Singleton)             │
├─────────────────────────────────────────────────────┤
│                                                     │
│  📁 Persistent Storage (Disk)                      │
│  ├─ image-cache/trade_photo_9_aad6a00c7dfe5175.jpg│
│  ├─ image-cache/trade_photo_9_7df6fe35d8c155c7.jpg│
│  └─ image-cache/cache-metadata.json               │
│                                                     │
│  💾 In-Memory Metadata (Fast Lookup)               │
│  {                                                  │
│    "trade_photo:9:aad6a00c7dfe5175": {            │
│      filePath: "/path/to/cached.jpg",             │
│      size: 524288,  // bytes                      │
│      lastAccessed: 1737483422000,  // timestamp   │
│      createdAt: 1737483000000      // timestamp   │
│    }                                                │
│  }                                                  │
└─────────────────────────────────────────────────────┘

LRU Eviction Flow

// Cache at 95MB → Download 10MB image → Cache now 105MB
// → Find least recently accessed images
// → Delete oldest accessed images (15MB worth)
// → Cache back to 90MB ✨

Handling Expired URLs

// 1. App tries to cache image using expired presigned URL
// 2. Gets 403 Forbidden (URL expired)
// 3. Cache logs the failure but doesn't crash
// 4. Next time trade list refreshes, backend sends fresh presigned URL
// 5. Cache successfully downloads and stores the image
// 6. Future requests use cached bytes (even if URL expires again)

🎨 Components

<CachedImage />

Main component for displaying cached images.

Props:

| Prop | Type | Required | Description | |------|------|----------|-------------| | url | string \| undefined | Yes | Pre-signed URL (expires) | | cacheKey | string \| undefined | Yes | Stable identifier (S3 key) | | fallbackUrl | string | No | Fallback image URL | | style | ImageStyle | No | Image style | | containerStyle | ViewStyle | No | Container style | | className | string | No | NativeWind className | | resizeMode | ImageContentFit | No | Resize mode (default: 'cover') | | alt | string | No | Accessibility label | | fallbackComponent | ReactNode | No | Custom fallback when no source | | shimmerColors | [string, string, string] | No | Custom shimmer colors | | backgroundColor | string | No | Custom background color |

<ImageWithShimmer />

Lower-level component with shimmer loading effect.

🪝 Hooks

useCachedImage(options)

React hook for cached image source.

Options:

interface UseCachedImageOptions {
  url: string | undefined;        // Pre-signed URL
  cacheKey: string | undefined;   // Stable cache key
  fallbackUrl?: string;           // Fallback URL
}

Returns:

interface UseCachedImageResult {
  source: string | null;          // Image source URI
  isLoading: boolean;             // Is downloading/caching
  isCached: boolean;              // Is from local cache
  refresh: () => Promise<void>;   // Force re-download
}

usePreCacheImages(items)

Pre-cache multiple images in background.

Parameters:

items: Array<{
  url: string | undefined;
  cacheKey: string | undefined;
}>

🛠️ API Reference

imageCacheService

Singleton instance with default configuration.

Methods

  • getCachedImage(cacheKey: string): Promise<string | null>
  • downloadAndCacheImage(url: string, cacheKey: string): Promise<string | null>
  • getImageSource(url: string | undefined, cacheKey: string | undefined): Promise<string | null>
  • preCacheImages(items: PreCacheItem[]): Promise<void>
  • clearImageCache(): Promise<void>
  • getCacheSize(): Promise<number>
  • deleteCachedImage(cacheKey: string): Promise<void>
  • getCacheStats(): Promise<CacheStats>
  • updateConfig(config: Partial<ImageCacheConfig>): void

📊 Cache Statistics

const stats = await imageCacheService.getCacheStats();

console.log(stats);
// {
//   totalImages: 127,
//   totalSizeBytes: 89456231,
//   totalSizeMB: 85.31,
//   oldestImageDate: 1734567890000,
//   newestImageDate: 1737483422000
// }

🧪 Testing

In your app:

// Enable debug logging
imageCacheService.updateConfig({ enableDebugLogging: true });

// You'll see logs like:
// [ImageCache] 📋 Loaded metadata for 23 images
// [ImageCache] ✅ Already cached, returning: /path/to/image.jpg
// [ImageCache] ⬇️ Starting download for: trade_photo:9:abc123
// [ImageCache] Cache size exceeded, starting eviction...
// [ImageCache] Evicted 5 images

📝 Real-World Example

This package was born from a real production app (TradeBay) struggling with S3 presigned URLs:

// Before: Images re-downloaded every 15 minutes, no offline support
<Image source={{ uri: trade.serviceImageUrl }} />

// After: Cached forever, works offline, Instagram-like UX
<CachedImage
  url={trade.serviceImageUrl}
  cacheKey={trade.serviceImage}
  style={styles.image}
/>

Results:

  • ✅ 95% reduction in S3 bandwidth costs
  • ✅ Images load instantly after first view
  • ✅ Full offline support (like Instagram)
  • ✅ Zero 403 errors from expired URLs
  • ✅ Automatic cache management (no manual cleanup needed)

⚠️ Android Memory Optimization (CRITICAL)

This package has been optimized to prevent Out-Of-Memory (OOM) crashes on Android based on production testing with apps experiencing up to 1.7GB memory usage leading to app crashes.

Memory Fixes Applied

The default configuration has been optimized for Android:

{
  maxCacheSizeMB: 10,           // Reduced from 100
  maxCacheAgeDays: 1,           // Reduced from 30
  maxConcurrentDownloads: 1,    // Reduced from 3
  enableDebugLogging: false     // Disabled - logs consumed 1GB+ via Sentry breadcrumbs
}

Root Causes Identified & Fixed

  1. Excessive Console Logging → All console.logs removed (were captured by Sentry breadcrumbs)
  2. Circular Dependency → Fixed loadCacheMetadataensureCacheDirectory infinite loop
  3. Cache Metadata Loss → Fixed module-level variables resetting on hot reload

Additional Recommendations for Android

When using this package with expo-image or other image libraries, implement these critical memory optimizations:

1. Use Disk-Only Caching

import { Image } from 'expo-image';

<Image
  source={{ uri: imageUri }}
  cachePolicy="disk"  // NOT "memory-disk"
  contentFit="cover"
/>

2. Enable Native Downsampling

<Image
  source={{ uri: imageUri }}
  style={{ width: 100, height: 100 }}
  // CRITICAL: Force native downsampling
  contentFit="cover"
  transition={200}
/>

3. Clear Memory Cache Periodically

import { Image as ExpoImage } from 'expo-image';
import { useEffect } from 'react';

// In your screen component
useEffect(() => {
  // Clear decoded bitmaps every 30 seconds
  const interval = setInterval(() => {
    ExpoImage.clearMemoryCache().catch(() => {});
  }, 30000);

  return () => {
    clearInterval(interval);
    // Also clear on unmount
    ExpoImage.clearMemoryCache().catch(() => {});
  };
}, []);

4. Disable Pre-Caching on Android

// ❌ DON'T pre-cache on Android (causes 700MB-1GB memory usage)
// await imageCacheService.preCacheImages(items);

// ✅ Let images cache on-demand as they scroll into view

5. Monitor Memory with ADB

# Monitor memory usage
adb logcat | grep -E "tradebay|lowmemory|GC"

# Check for lowmemorykiller events
adb logcat -d | grep lowmemorykiller

Sentry Configuration (If Using)

Disable memory-heavy features:

Sentry.init({
  debug: false,                        // Logs every touch event
  attachScreenshot: false,             // Consumes significant memory
  attachViewHierarchy: false,          // Can use 100MB+ on complex screens
  enableAutoPerformanceTracing: false, // Touch event tracking
  tracesSampleRate: 0.1,              // Reduced from 1.0
  maxBreadcrumbs: 5,                  // Reduced from 100

  beforeBreadcrumb(breadcrumb) {
    // Drop touch/gesture breadcrumbs
    if (breadcrumb.category === 'touch' || breadcrumb.category === 'gesture') {
      return null;
    }
    // Only keep error/warning console logs
    if (breadcrumb.category === 'console') {
      return breadcrumb.level === 'error' || breadcrumb.level === 'warning'
        ? breadcrumb
        : null;
    }
    return breadcrumb;
  },
});

React Query Configuration

Reduce cache times to free memory faster:

new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 2 * 60 * 1000,  // 2 minutes (reduced from 5)
      gcTime: 3 * 60 * 1000,     // 3 minutes (reduced from 10)
    },
  },
});

Expected Memory Usage

After applying these fixes:

  • Before: 700MB - 1.7GB (killed by Android lowmemorykiller)
  • After: 100MB - 200MB (stable)

🤝 Contributing

Contributions are welcome! Please open an issue or PR.

📄 License

MIT © Kayode (kayzmann)

🙏 Acknowledgments

This package was inspired by real pain points encountered while building production React Native apps with S3 image storage. Special thanks to the discussions and research that led to this solution.

🔗 Links


Built with ❤️ by kayzmann

If this package saves you from S3 presigned URL headaches, give it a ⭐ on GitHub!