Wallpaper apps live and die by one thing: how fast images appear on screen. I've been building apps in this category since 2014, and across tens of millions of downloads, the single most consistent complaint in 1-star reviews isn't missing features or crashes — it's "it's slow." Users don't usually explain what's slow. They just leave.
When I built a wallpaper app prototype with Rork, the generated code used React Native's standard <Image> component throughout. The app looked right in the Rork Companion preview. But testing on a physical device told a different story: images were taking 400–600ms to appear, and every scroll produced a flash of white. The images would pop in rather than feel ready.
Rork's generated code wasn't wrong — it was using the correct React Native API. The issue is that React Native's built-in Image component has limitations that don't matter much for simple apps but become very visible once you're loading dozens of high-resolution images in a scroll view.
The fix was switching to expo-image. Here's exactly what I did and what changed.
Why React Native's Default Image Component Causes This
Rork generates standard React Native code, which means <Image> from react-native unless you tell it otherwise. This component is functional, but it has three weaknesses that compound in image-heavy apps:
Weak memory caching. Images scrolled off-screen can be evicted from memory cache and need to re-download when they scroll back into view. In a wallpaper grid, users scroll back to images they liked — and seeing that white flash a second time feels broken.
No progressive display. Nothing is shown until the full image is downloaded. For high-resolution wallpapers on a mobile connection, this creates a visible gap between "list appeared" and "images appeared."
No built-in placeholder support. You can implement a custom loading state, but it requires extra state management and adds complexity that Rork then has to maintain in future edits.
For an app where the core experience is "scroll and look at beautiful images," these limitations directly reduce the perceived quality — even if everything else works perfectly.
What expo-image Does Differently
expo-image is the Expo team's drop-in replacement for the standard Image component, designed specifically to address the limitations above.
The key differences that matter for wallpaper apps:
LRU disk cache. Once an image is downloaded, it's written to disk. On subsequent views — even after the app is closed and reopened — it loads from disk at near-instant speed. This is the biggest single improvement for repeat-scroll behavior.
Blur hash placeholder. While the image downloads, a blurred color preview is rendered immediately from a short hash string. Users see "something" within milliseconds — a warm blur that resolves into the actual image. This changes the perceived experience from "waiting for nothing" to "image appearing."
Smooth crossfade. Instead of an abrupt pop-in, images fade in over a configurable duration when they finish loading. 200ms is enough to feel polished without being noticeable.
Progressive JPEG support. For progressively-encoded JPEGs, the image renders at increasing quality as data arrives.
How to Tell Rork to Use expo-image
For new projects, add this to your prompt before generating the wallpaper list screen:
Use expo-image's Image component for all image display.
Do not use React Native's built-in Image.
Set cachePolicy="memory-disk" on all image components.
Add a placeholder blurhash prop to each image item.
For an existing Rork project, ask it to update:
Replace all instances of <Image source={{ uri: ... }} />
with the expo-image Image component.
Add cachePolicy="memory-disk" and placeholder={{ blurhash: "..." }}
to each one. Keep the same styles.
After Rork generates the updated code, check that expo-image appears in package.json under dependencies. If it doesn't, Rork may have imported from the wrong package — ask it to install expo-image explicitly.
Code Comparison: Before and After
Before — Rork's default output:
import { Image } from 'react-native';
function WallpaperItem({ uri }: { uri: string }) {
return (
<Image
source={{ uri }}
style={{ width: '100%', height: 200 }}
resizeMode="cover"
/>
);
}After — with expo-image:
import { Image } from 'expo-image';
// Fallback blur hash: a warm neutral gray
// Replace with per-image blur hashes when available
const FALLBACK_BLURHASH = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4';
function WallpaperItem({ uri, blurhash }: { uri: string; blurhash?: string }) {
return (
<Image
source={{ uri }}
style={{ width: '100%', height: 200 }}
contentFit="cover" // same as resizeMode="cover"
placeholder={{ blurhash: blurhash ?? FALLBACK_BLURHASH }}
transition={200} // 200ms crossfade on load
cachePolicy="memory-disk" // cache to both memory and disk
/>
);
}The API is nearly identical. contentFit maps directly to resizeMode values ("cover", "contain", "fill", etc.), so the style logic doesn't need to change. The migration is mostly a find-and-replace with a few additions — typically under an hour for a full app.
Generating Blur Hashes
Blur hashes are short strings that encode the dominant color structure of an image. expo-image decodes this string on-device in real time to render a blurred color preview. The string itself is around 20–30 characters — small enough to include in API responses without meaningful overhead.
The right place to generate blur hashes is server-side, at the time images are uploaded. Store the hash alongside the image URL in your database, then include it in the API response that your app fetches.
Node.js server-side generation:
import { encode } from 'blurhash';
import sharp from 'sharp';
async function getBlurhash(imagePath: string): Promise<string> {
const { data, info } = await sharp(imagePath)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' }) // downscale for faster processing
.toBuffer({ resolveWithObject: true });
return encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4, // x components — controls horizontal detail
4 // y components — controls vertical detail
);
}If your wallpaper content is stored in Cloudflare R2 and processed through Cloudflare Workers, you can generate the blur hash in the upload Worker and store it in D1 or KV. This way, every image in your API response already includes its hash, and your app always has something to show immediately.
If you don't have per-image hashes yet, start with a single fallback hash for all images. Even a generic neutral placeholder is better than a white flash. Migrate to per-image hashes incrementally as your upload pipeline is updated.
What Changed After Switching
After migrating to expo-image and testing on a physical device through Rork Companion:
Scroll behavior improved noticeably. Blur hash placeholders appeared immediately as list items rendered — users saw a colored blur instead of a white rectangle. This alone changed the perceived responsiveness significantly.
Scroll-back no longer showed re-loading images. The LRU disk cache kept recently-viewed images available, so scrolling back to something seen 20 seconds earlier felt instant rather than triggering a re-download.
The overall session felt more stable. With fewer white flashes interrupting the visual flow, the app felt like a finished product rather than a prototype.
These are observations from real-device testing, not synthetic benchmarks. But after years of maintaining apps with tens of millions of downloads, I've learned to trust what behavioral metrics and reviews say over what looks right in a simulator. The "it's slow" complaints in app reviews tend to disappear when you address the actual bottlenecks — and image loading is one of the most common.
A Note on AdMob Integration
If your wallpaper app uses AdMob interstitial or rewarded ads, faster image loading has a secondary benefit: users spend more time scrolling before they're likely to close the app. Since AdMob impression revenue correlates directly with session depth, reducing friction in the core browsing experience is also an indirect revenue improvement. This is something I've seen repeatedly with the apps I've built — the relationship between UX quality and ad revenue is more direct than it might seem.
Related Articles
For overall app performance beyond images, the Rork App Performance Optimization Guide covers rendering, memory, and bundle size in detail.
If your wallpaper list uses FlatList and performance is still not where you want it, Migrating from FlatList to FlashList in Rork is a natural next step.
If you're starting a wallpaper app from scratch, Building and Publishing a Wallpaper App with Rork covers the full process from idea to App Store.
Start by replacing one <Image> component with expo-image, set a fallback blur hash, and test it on a physical device. The difference in perceived speed is usually visible within a minute. Once you see it, migrating the rest of the app becomes easy to justify.