React bindings for image-cache-pro — paint image-heavy UIs on low-power devices without dropping frames.
The core engine decodes images ahead of time, warms GPU textures a few per
frame on requestAnimationFrame, yields to user input, and evicts against
RAM/GPU budgets you set. This package maps it onto React:
<ImageCacheProvider> one engine per app — budgets, frame queue, input gate
<BucketProvider> one bucket per UI region — rail, page, virtual list
<CachedImage /> one image at one size (or useImage() for headless)
Everything is typed end to end, react is a peer dependency, nothing is
bundled, and the components are built for Chrome-88-class embedded browsers
(Cobalt / smart TV): <div>-based rendering, explicit dimensions, and
re-renders coalesced to at most one per frame.
npm install image-cache-pro-reactimport {
ImageCacheProvider,
BucketProvider,
CachedImage,
} from 'image-cache-pro-react'
function App() {
return (
<ImageCacheProvider
ram={200}
video={60}
units="MB"
loaders={4}
canRender={() => !window.myApp.isNavigating} // input-yield gate
>
<BucketProvider name="top-rail" priority={1}>
{posters.map(poster => (
<CachedImage
key={poster.id}
url={poster.url}
width={320}
height={180}
className="poster"
/>
))}
</BucketProvider>
</ImageCacheProvider>
)
}CachedImage stays a colored placeholder until the engine has decoded the
bitmap and warmed the GPU texture, then reveals it in one cheap paint —
the reveal is the scheduling gate. Style states via the data-state
attribute (idle | loading | queued | rendered | evicted | error):
.poster {
opacity: 0.35;
transition: opacity 250ms ease-out;
}
.poster[data-state='rendered'] {
opacity: 1;
}Owns one engine Controller for the subtree. SSR-safe and StrictMode-safe
(the engine is created in an effect and fully cleared on unmount).
- All
ControllerPropsare accepted:ram,video,units,loaders,frameBudget,hwRank,canRender,renderer,gpuDataFull… - Live props:
ram/video(runtime budget setters — e.g. shrink image memory while media plays) andcanRender(the consumer-owned input gate). Everything else is construction-time. onController={c => …}— imperative escape hatch for input layers living outside React.
Imperative engine access: controller.pause() / resume(),
controller.setVideoBudget(n), stats, the frame queue.
Owns one Bucket for a UI region. priority, lock and videoBudget are
live — changing priority re-sorts pending work on the next frame (a
focused rail jumps the queue). Unmount clears every request in the region.
useBucket for imperative control (bucket.pause(), setPriority).
useBucketState for UI state — { loaded, loading, rendered, loadProgress, requestCount } — with re-renders coalesced to one per frame regardless
of how bursty the underlying events are.
The headless hook behind CachedImage:
const { status, rendered, src, progress, error, request } = useImage({
url, // image identity
width,
height, // target size (required — enables the decoder bypass)
priority, // live — re-sorts the frame queue
visible, // default true: mounted images are eviction-locked;
// pass false for off-screen precache that may evict
trackProgress, // opt-in progress re-renders
})The request is created per url+width+height (object identity of your props
never churns it) and force-cleared on unmount — deterministic teardown,
virtual-list safe. Eviction under memory pressure surfaces as
status: 'evicted'; the library never re-requests automatically (no
re-request storms) — remount or change a key to re-request.
useImage + a device-friendly <div> renderer: explicit width/height
(embedded browsers paint auto-sized boxes as 0×0), background-image reveal
on the real on-screen node, fit (fill | cover | contain),
placeholderColor, overlay children, onRendered/onError, all DOM
props forwarded, ref to the div.
The full image-cache-pro surface is re-exported (Controller, Bucket,
RenderRequest, all types) so a single import serves both layers.
Focus-driven priorities (TV rails):
<BucketProvider name="rail" priority={focusedRow === index ? 10 : 0}>Pause warming during navigation (input layer owns the gate):
<ImageCacheProvider canRender={() => !inputBusy()} … >
// or imperatively:
const controller = useController()
controller?.pause() // route transition start
controller?.resume() // idleShrink image memory while video plays:
<ImageCacheProvider video={isPlaying ? 32 : 240} units="MB" … >Virtual lists — one long-lived BucketProvider for the list; mount and
unmount CachedImage slots freely. Re-visiting a cached URL costs no
network and no decode, only a budgeted re-warm.
Live: https://savanesoff.github.io/image-cache-pro-react/
pnpm storybook runs it locally — including the Rails demo (the STB
workload with live budget/priority controls) and a memory-pressure story
where you can watch eviction happen.
pnpm install
pnpm test # vitest — full-stack through the real engine
pnpm lint # prettier + eslint (type-checked)
pnpm run type-check
pnpm run build # ESM + CJS + .d.ts (react + core stay external)
pnpm storybookMIT — © Samvel Avanesov