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

react-native-true-image

v2.2.1

Published

A single-layer, cache-key-stable image component for React Native

Readme

react-native-true-image

A single-layer image component for React Native with a cache-key contract between sized prefetch requests and the view, so matching images mount as synchronous memory hits. Oversized bitmaps are downsampled during decoding. Built as a Fabric component and TurboModule on SDWebImage (iOS) and Glide (Android).

One native view draws one image on one layer (iOS) or one canvas (Android). No subviews, no placeholder views.

Installation

yarn add react-native-true-image

Requires React Native 0.76 or later with the new architecture enabled. Expo apps need a development build; the package works with expo prebuild without a config plugin.

Usage

import { Image } from 'react-native-true-image';

<Image
  source="https://cdn.example.com/cover.jpg"
  recyclingKey={item.id}
  transition={300}
  style={{ width: 120, height: 120, borderRadius: 8 }}
  onDisplayEnd={() => setFallbackVisible(false)}
/>;

await Image.prefetch(covers.map((c) => c.url));

Sources

| Input | What native receives | Path | | --- | --- | --- | | Remote URL | the string as-is | SDWebImage / Glide through OkHttp | | { uri, headers, cacheKey } | the URI plus a header list and cache key | same, with the headers on the request. See Variants of one picture | | require() asset | Image.resolveAssetSource().uri | file loader / drawable | | Scheme-less name, e.g. "ic_play" | the bare name | iOS asset catalog / Android drawable |

Scheme-less names are drawn in the same frame and never fade, which makes them suitable for icons. tintColor applies to them only.

Headers are sent with the request and are not part of the cache key: the same URL is one cached image whatever headers fetched it, so a prefetch with headers and a view without them still share one entry. Pass the same { uri, headers } object to both when the server needs them.

Borders and rounded corners

The view takes every borderRadius, borderWidth, borderColor and borderStyle prop a View takes, on both platforms, and clips the image to the border box. There is no need for a clipping wrapper.

<Image source={url} style={{ width: 96, height: 96, borderRadius: 12 }} />

Clipping relies on the default overflow: 'hidden'; overriding it to visible on iOS draws the image square.

Props

| Prop | Default | Notes | | --- | --- | --- | | source | | string, { uri, headers, cacheKey }, require() id, or null to clear | | resizeMode | 'cover' | cover, contain, stretch, center | | downsampleThreshold | 2 | Linear oversize factor before decode-time downsampling. 1 always downsamples oversized images; 0 disables it | | transition | 300 ms for URLs, 0 for assets | Fade duration in milliseconds | | blurRadius | 0 | In source-image pixels, so the same value looks the same on both platforms | | blurPixelsPerRadius | 2 | Pixels the blur radius spans after the pre-blur shrink; 0 blurs at full size. See Blur cost | | tintColor | | Native resources only | | recyclingKey | | Clears the view synchronously when it changes, before the next source loads | | placeholder | | Shown into an empty view until source loads. Same shapes as source. See Placeholder | | placeholderTransition | same as transition | Crossfade from the placeholder to the image, in milliseconds; 0 cuts | | placeholderPolicy | 'cache-only' | cache-only shows a remote placeholder only if already cached; network fetches it |

Events

  • onLoad({ width, height, source }): the image decoded. Sizes are pixels. With a cacheKey the image may be a reused variant, so the size can differ from the URL's; source is still the URL the view asked for.
  • onError({ error, source }): the load failed. Not fired for a load cancelled by a newer source.
  • onDisplay(): the image was handed to the view, at the start of a fade or on draw.
  • onDisplayEnd(): the image is fully drawn, after the fade or together with onDisplay when there is none.

A fade softens an image arriving late. A memory-cached image arriving into an empty view draws at once regardless of transition. Replacing a displayed image always crossfades. An image whose fade is interrupted never reports onDisplayEnd; the newer image reports in its turn.

Prefetch

Image.prefetch(source | source[], options?) takes URL strings or { uri, headers, displaySize?, resizeMode?, downsampleThreshold? } objects and resolves true only if every image loaded. Options apply to the whole batch; per-source options override them.

await Image.prefetch(covers.map((cover) => cover.url), {
  displaySize: { width: 100, height: 100 },
  resizeMode: 'cover',
  downsampleThreshold: 2,
});

<Image source={cover.url} style={{ width: 100, height: 100 }} />

displaySize uses React Native layout units, like style.width and style.height. Native code converts to physical pixels using screen density. A view uses its measured size automatically and waits for nonzero layout before starting a sized load. Matching physical dimensions, resize mode and threshold share a decoded memory-cache entry. Estimates also work: a view reuses an existing variant when it has enough detail and is no more than the threshold above the required resolution. A full-resolution source that is smaller than the view can also be reused. A reduced variant that would need upscaling, an overly large variant, or changed decode settings require a new decode. Headers remain outside the key. Original downloaded data stays shared by URL on disk. Different sizes can therefore decode from disk without downloading the image again.

Rendering defaults to downsampleThreshold={2}. When an image has more than twice the required resolution on each axis, it decodes near its displayed pixel size. Exactly 2× keeps the original; the threshold is linear, so 2× on both axes means four times the pixels. cover accounts for the portion outside the view, contain for the fitted image, and stretch conservatively retains enough detail on both axes. center and native vectors do not participate in downsampling. Images are never upscaled during decoding. The decoder may round the resulting dimensions, and onLoad reports the actual decoded pixel dimensions.

Set downsampleThreshold={0} to retain the original-resolution behavior. Thresholds must be finite and either 0 or at least 1; display dimensions must be finite and positive. Prefetching without displaySize still loads the original resolution, but does not guarantee a synchronous memory hit for a sized view. To keep the previous URL-only prefetch contract, opt out of downsampling on the view as well.

On Android the prefetch loop runs on a background thread: each load is started with submit() and awaited there, and the main looper sees a single post per batch. Prefetches usually race the mount work they feed, so they stay out of its queue.

Colours

Image.getColors(source, options?) picks colours from an image, for example to tint a player with its album artwork. It accepts the same shapes as source; options.headers are sent with the request, and a { uri, headers } source's own headers win over them.

const colors = await Image.getColors(track.artworkUrl);
if (colors) {
  setTint(colors.darkVibrant ?? colors.darkMuted ?? colors.dominant);
}

It resolves an ImageColors object, or null if the image could not be loaded or decoded, or has no opaque pixels:

| Field | Notes | | --- | --- | | dominant | The most common colour after the filter below, as in Palette: a mostly black cover reports its most common other colour. The mean colour when the filter leaves nothing. Never null | | vibrant, lightVibrant, darkVibrant | Saturated colours at mid, light and dark lightness | | muted, lightMuted, darkMuted | Desaturated colours at mid, light and dark lightness |

Every colour is a #rrggbb string. The six named swatches are null when no colour in the image fits them, and a colour fills one of them at most.

The image loads through the same request as a view or prefetch of that URL, so a cached image is not downloaded again. Native code decodes it near 48 × 48 pixels and stretches it to that square; the palette is computed in JS from those pixels, so both platforms pick colours the same way. The algorithm follows Android's Palette: transparent pixels are skipped, near black, near white and skin-like reds are filtered out, the rest is reduced to 16 colours by median cut, and each colour is scored against the six targets by saturation, lightness and how much of the image it covers. Swatch colours are the mean of the pixels they cover, so a flat colour comes back exactly. Scaling filters differ slightly between the platforms, so the same image can give colours a level or two apart.

Variants of one picture: cacheKey

Some servers hand out one picture at several URLs, one per size, such as an image proxy's …/cover/42?size=80, ?size=256 and ?size=512. Each URL is cached on its own, so without help a 512 px album header loads from scratch although the 256 px list row it came from is in memory. Give such URLs a shared cacheKey to say they are the same picture:

const artwork = (size: number) => ({
  uri: `${server}/imageproxy/${album.id}?size=${size}`,
  cacheKey: `album:${album.id}`,
});

<Image source={artwork(256)} style={{ width: 80, height: 80 }} />;
<Image source={artwork(1024)} style={{ width: 320, height: 320 }} />;

A view whose source has a cacheKey:

  1. Reuses a variant instead of fetching. If another URL with the same key is cached at a resolution that fills the view without upscaling, it is shown and the requested URL is not fetched. "Fills" means at least the view's pixel size on both axes for cover and stretch, and on the axis the image fits by for contain. It decodes under the usual downsampleThreshold policy. A variant whose decode for this view is already in memory draws at once, exactly like an exact memory hit; otherwise it decodes from the disk cache without touching the network.
  2. Stands a smaller variant in while loading. Otherwise the requested URL loads as usual, and the largest cached variant that is too small is shown as a placeholder in the meantime: at once when it is in memory, after a disk read otherwise. The image then crossfades in over placeholderTransition, exactly as from a placeholder.
  3. Changes nothing else. The exact URL is still preferred when it is cached. The order is: the URL in memory, a sufficient variant in memory, the URL on disk, a sufficient variant on disk, the network. Sources without a cacheKey, resizeMode="center" and native resources behave as before. A view needs its layout to judge sufficiency; before it has one, a variant can only stand in.

The variant counts as the image: it reports onLoad, onDisplay and onDisplayEnd like any other, with the variant's pixel size in onLoad. A smaller stand-in is a placeholder and reports nothing. recyclingKey clears and interrupted fades work as for any image. Headers stay out of the key: a variant is requested with the headers of the source asking for it.

Image.prefetch with a cacheKey registers the URL as a variant, and Image.getColors samples a cached variant instead of fetching, preferring the smallest that covers its 48 px sample.

Each platform keeps an index from cache key to the URLs seen with it and each URL's full pixel size, recorded whenever a load or prefetch with a key succeeds. It holds no pixels. It is stored as a small JSON file in the app's caches directory so it survives restarts, written off the main thread a second after a change, and bounded to 2000 keys (least recently used are dropped) and 8 URLs per key. An unreadable file is discarded. The index never claims a cache hit on its own: each variant is looked up cache-only, and one the image cache has evicted is forgotten and the next choice is tried, ending with the network for the requested URL.

Placeholder

A placeholder stands in while source loads: a low-resolution copy that a list row already cached, or a bundled asset. It shows only into an empty view, which is on mount, after a recyclingKey change, or after source was cleared. A displayed image is never swapped for a placeholder while its replacement loads. The placeholder draws the moment it is available, never fades in, and reports no events, so onLoad and friends always describe source. When the image arrives it crossfades over placeholderTransition or cuts at 0. An image that is a memory hit shows at once and the placeholder is never loaded.

With a cacheKey on source, a cached smaller variant of the same picture is preferred over a remote cache-only placeholder, which is still tried if no variant is cached. A local placeholder, or a remote one with the network policy, is the app's explicit choice and is used instead.

placeholderPolicy decides where a remote placeholder may come from. cache-only reads the memory and disk caches and gives up silently otherwise, so it never costs a request. network fetches it like any other image, through the same request as a view or prefetch of that URL, so it lands in the shared cache. Local placeholders always show. blurRadius and resizeMode apply to the placeholder as well.

Blur cost

A Gaussian blur erases every detail finer than its radius, so blurring a copy shrunk until the radius spans a couple of pixels looks the same as blurring the original. blurPixelsPerRadius sets how many pixels that is. With the default of 2, a radius of 100 shrinks the image fifty times per axis and blurs it with a radius of 2. Raise it to keep more detail in the blurred image, or set it to 0 to blur at full size.

Both platforms blur once, off the main thread, and then draw the blurred bitmap like any other. A crossfade between blurred images is two bitmap draws per frame, with no per-frame filter: a blur is linear, so fading between two blurred images is pixel-identical to blurring the fade. A blurred image is never shown sharp first; it appears once the blur is done.

On iOS the shrink runs inside the SDWebImage transform and the result is cached under a key that includes the shrink factor. The sharp original stays cached and is the transform input, so a blurred view still reuses a prefetch. On Android the view blurs the drawable Glide hands it, so the request and its memory cache entry stay identical to the prefetch, and a small cache keyed on source, radius and factor makes the blur a synchronous hit for recycled rows. The two blurs use the same three-pass box approximation of a Gaussian, so one radius looks the same on both.

Android: parallel decoding of disk-cached images

Glide decodes disk-cached images on a single thread by default. Fresh downloads decode on a pool of up to four threads, but a DecodeJob for an image already on disk runs on the disk cache executor, which has one thread. A cold scroll through a list whose images are on disk decodes them one at a time.

Measure before opting in. On one launch profile with twenty prefetched covers, decode was under a third of the batch's wall time and adding threads changed time-to-paint by under one percent; the rest was main-thread scheduling, which the background prefetch loop now avoids. The helper helps a cold scroll of disk-cached images that were not prefetched, and little else.

Executors can only be configured through the app's AppGlideModule, so this package cannot change it on its own. To opt in, add the Glide annotation processor to your app and call the helper from applyOptions:

import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
import com.trueimage.TrueImageGlideOptions

@GlideModule
class AppGlide : AppGlideModule() {
  override fun applyOptions(context: Context, builder: GlideBuilder) {
    TrueImageGlideOptions.apply(builder)
  }
}

apply sizes the disk cache executor like Glide's source executor, min(4, CPU cores). Pass a threadCount to choose differently.

Development

yarn            # install
yarn test       # JS contract tests
swift test      # iOS geometry and policy tests, runs on the host
yarn example android
yarn example ios

Android unit tests (pure policy plus Robolectric tests of the view and the prefetch contract against Glide) run with the example's Gradle wrapper:

yarn example test:android

iOS view and prefetch tests run as an XCTest bundle hosted by the example app, against SDWebImage with a fake network, on the first available iPhone simulator (the pods must be installed in example/ios):

yarn example test:ios

CI runs all of these, plus the example builds, on every push and pull request.

A Nix flake provides the Android toolchain: nix develop for the SDK and JDK, nix develop .#emulator to include an emulator image.

License

MIT