If you've built more than one React app, you've probably written the same image-loading boilerplate more than once.
A skeleton here. A lazy-loading IntersectionObserver there. A fallback image when the CDN hiccups. A blur placeholder to avoid layout shift. Retry logic for flaky hosts.
Every new React project ends up with the same image-loading code—copied from the last project, tweaked a little, and rarely perfect.
Sound familiar?
@concatstring/react-smart-image brings all of those features together in a single component. Simply swap <img> with <SmartImage> and opt into features like lazy loading, WebP detection, blur placeholders, skeleton loaders, retries, responsive images, and performance metrics—all without changing your existing code.
Getting started only takes a minute:
npm install @concatstring/react-smart-image
Key Features
- ✅ Drop-in replacement for
<img> - ✅ Zero runtime dependencies
- ✅ Fully typed with TypeScript
- ✅ Responsive image support
- ✅ Lazy loading
- ✅ Skeleton loaders
- ✅ Blur placeholders
- ✅ Retry with exponential backoff
- ✅ Error fallbacks
- ✅ Automatic WebP detection
- ✅ Image performance metrics
- ✅ Works with React 18+
Why We Built React Smart Image
Across multiple React projects, we found ourselves implementing the same image-loading logic repeatedly. Some projects needed lazy loading, while others required blur placeholders, retry logic, responsive images, or skeleton loaders. Maintaining separate implementations for each project quickly became repetitive and difficult to scale.
We wanted a single component that behaves like a normal <img> while providing modern image optimization features only when needed.
That's why we built React Smart Image.
Instead of maintaining multiple utilities across projects, I now use one component everywhere and simply enable the features I need with a few props. The result is cleaner code, a more consistent developer experience, and better image performance out of the box.
Drop-in Replacement
Replacing your existing images is as simple as swapping the component:
import { SmartImage } from '@concatstring/react-smart-image';
<SmartImage src="/photo.jpg" alt="A photo" width={800} height={600} />
className, style, onClick, loading, ref — all forwarded to the real <img>. Nothing new to learn. Enable additional features only when you need them—one prop at a time.
1. Lazy loading that actually waits for the viewport
<SmartImage src="/hero.jpg" alt="Hero" width={1200} height={600} lazy />
Images load 100px before they scroll into view — powered by IntersectionObserver, no scroll listeners, no jank.
2. Skeleton loaders (with dark-mode support)
<SmartImage
src="/avatar.jpg"
alt="User avatar"
width={80}
height={80}
skeleton
skeletonColor="#1f2937"
skeletonHighlightColor="rgba(255,255,255,0.12)"
/>
An animated shimmer while the image loads — themeable to your brand or dark UI.
3. Blur placeholders — manual or automatic
Pass a tiny base64 LQIP the Next.js way:
<SmartImage src="/landscape.jpg" alt="Landscape" width={1200} height={800}
placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." />
Or skip the base64 entirely. If your CDN resizes by URL (Cloudinary, imgix, CloudFront…), autoBlur derives the preview for you:
<SmartImage src="/landscape.jpg" alt="Landscape" width={1200} height={800}
placeholder="blur" autoBlur blurWidth={24} />
4. Retry with exponential backoff + fallback
CDNs fail. Handle it declaratively instead of with a tangle of onError handlers:
<SmartImage
src="/flaky-cdn.jpg"
alt="Product"
width={400}
height={300}
retry={3}
retryDelay={500} // 500ms → 1s → 2s
fallback="/placeholder.png"
/>
5. WebP auto-detection
Try the modern format first, fall back automatically if it 404s:
<SmartImage src="/photo.jpg" alt="Product" width={600} height={400} webp />
6. Responsive images — two strategies
The default srcset strategy lets the browser pick (DPR-aware). The viewport strategy always serves the smallest bytes per screen, ignoring device pixel ratio:
<SmartImage
src="/banner.jpg"
alt="Banner"
responsive
strategy="viewport"
sizes={{ mobile: 480, tablet: 768, desktop: 1200 }}
/>
| Screen |
srcset (default) |
viewport |
|---|---|---|
| Mobile @1× | 480w | 480w |
| Mobile @2× | 1200w (DPR-aware) | 480w |
| Desktop | 1200w | 1200w |
Host encodes size in the path instead of ?w=? Point it wherever you want with srcSetBuilder:
srcSetBuilder={(src, width) => src.replace(/(\.\w+)$/, `-${width}$1`)}
// /photo.jpg → /photo-480.jpg
Compose everything
The features are orthogonal — stack them:
<SmartImage
src="/product.jpg"
alt="Product photo"
width={600}
height={600}
lazy
skeleton
webp
fallback="/placeholder.png"
retry={2}
retryDelay={1000}
onLoadInfo={({ loadTime, fromCache }) =>
console.log(`Loaded in ${loadTime}ms (cache: ${fromCache})`)
}
/>
That onLoadInfo callback hands you loadTime, naturalWidth/Height, and a fromCache flag — real performance telemetry per image, for free.
Why not just use next/image?
next/image is great — if you're on Next.js. react-smart-image works in any React app: Vite, CRA, Remix, Gatsby, a plain SPA. No image server required, no framework lock-in, no runtime dependencies. Think of it as a modern, framework-agnostic upgrade to the standard <img> element.
Try it
npm install @concatstring/react-smart-image
If React Smart Image saves you from writing the same image-loading code again, I'd really appreciate a ⭐ on GitHub.
Feedback, feature requests, and bug reports are always welcome. If there's a feature you'd like to see, let me know in the comments or open an issue.
Built by the team at Concatstring.
Top comments (1)
This is a solid direction for solving a real pain point in React apps—image performance is often underestimated until it breaks UX. What I like about this approach is the focus on abstraction: wrapping optimization, lazy loading, and responsive behavior into a reusable component instead of pushing that burden onto every developer. That said, image optimization is a deep space—CDN-level resizing, format negotiation (WebP/AVIF), and proper caching strategies still matter a lot beyond the React layer. Curious how this compares with Next.js Image or modern CDN-based solutions. Still, great idea for teams not using a full framework stack.