Choose your package
A decision table for picking the right ditherkit package for your job.
The toolkit offers two places to run the dithering work: in the
browser (@ditherkit/react) or on the server (@ditherkit/next). This
page is a decision table for picking between them.
The short version
- Interactive, user-adjustable, previews →
@ditherkit/react - Single static image, fixed size →
@ditherkit/nextwith<DitheredImageSSR /> - Responsive image that switches resolution per viewport →
@ditherkit/nextwith<DitheredPicture /> - Neither of those, or you're not using React →
@ditherkit/coredirectly
The full decision table
| If you need… | Use |
|---|---|
| Real-time slider adjustments by the user | @ditherkit/react |
| Live preview of a file the user just uploaded | @ditherkit/react |
| A playground where the user picks algorithm and palette | @ditherkit/react |
| A single dithered image at a fixed size | <DitheredImageSSR /> from @ditherkit/next |
| Responsive dithered images in grids, cards, fluid layouts | <DitheredPicture /> from @ditherkit/next |
| Different dither settings per viewport (art direction) | <DitheredPicture /> with per-variant overrides |
| Dithered images as part of your static marketing pages | @ditherkit/next |
| SEO-indexable dithered content | @ditherkit/next |
| CDN-cached, long-lived image URLs | @ditherkit/next |
Dithered <img> tags that work without JavaScript | @ditherkit/next |
| Zero client-side JavaScript for images | @ditherkit/next |
| A Node build script that outputs dithered files | @ditherkit/core + sharp |
| Cloudflare Workers / edge runtime | @ditherkit/core + a bundler-friendly image decoder |
| SvelteKit / Vue / Solid / non-React framework | @ditherkit/core directly |
| The smallest possible bundle | @ditherkit/core + your own integration |
DitheredImageSSR vs DitheredPicture
Both are in @ditherkit/next, both are server-rendered with zero
client JS. The difference is responsiveness:
<DitheredImageSSR /> | <DitheredPicture /> | |
|---|---|---|
| Renders | Single <img> via next/image | <picture> with multiple <source> |
| Responsive | CSS scaling only (width: 100%) | True resolution switching per viewport |
| Art direction | One set of dither params | Different params per breakpoint |
| Best for | Hero images, fixed-size slots | Grids, cards, fluid layouts |
Use <DitheredImageSSR /> when the image has one fixed output
resolution. Use <DitheredPicture /> when you need different
dithered variants at different viewport widths — especially in
layouts where CSS scaling would either waste bandwidth or show
blurry upscaled pixels.
See <DitheredPicture /> for the
full reference, including computeVariants for auto-generating
breakpoints from your layout function.
Why not "just both"?
You can absolutely use both in the same Next.js app — see the example-next app, which demonstrates exactly that. You'd want both when:
- Your landing page uses
@ditherkit/nextfor fast, cached, above-the-fold imagery - Your playground / upload / editor page uses
@ditherkit/reactfor interactive adjustments
The packages coexist cleanly. Just remember to install both:
pnpm add @ditherkit/next @ditherkit/react sharpAnd import from the matching package — @ditherkit/next does NOT
re-export @ditherkit/react.
Why not "just server"?
Server-side rendering is wonderful for anything static, but it can't do real-time updates. Every parameter change would require a new server round-trip, a new Sharp pass, and a new HTTP response. Even with ISR caching, the first render is slow and the feedback loop on a slider would be terrible.
The Web Worker model in @ditherkit/react processes images in the
browser in a few milliseconds per pass, with no network involvement
once the source image is cached. That's the right model for
interactivity.
Why not "just client"?
Client-side rendering means:
- The source image is always fetched by the browser. No server-side proxy for CORS-restricted sources.
- The processing runs on every user's device. Slow devices get slow rendering.
- The output never gets cached at the edge. Every user re-processes the image from scratch.
- No SEO. Search engine crawlers see a blank
<canvas>, not the dithered content.
For anything above the fold on a marketing page, or any image that's the same for every user, the server-rendered path is dramatically better.
Performance comparison
For a 400×300 image with Floyd-Steinberg dithering:
@ditherkit/react (client) | @ditherkit/next (server, first hit) | @ditherkit/next (server, cached) | |
|---|---|---|---|
| First paint | ~50ms (source fetch + decode + worker) | ~100ms (fetch + Sharp + network) | ~5ms (HTTP cache hit) |
| Slider drag update | ~10ms | N/A — no live interaction | N/A |
| Subsequent page loads | Full reprocess per load | Cache hit | Cache hit |
| Bandwidth per view | Source image size | WebP output size | 0 (cached) |
The client case is fast for the first interaction but re-processes on every page load. The server case has a slower first hit but every subsequent view is essentially free.
Rule of thumb: if an image is the same for every user and
doesn't change interactively, serve it with @ditherkit/next. If it
changes per-user or per-interaction, use @ditherkit/react.
When to use @ditherkit/core directly
Both @ditherkit/react and @ditherkit/next are opinionated wrappers
around @ditherkit/core. They're great if their opinions match your
use case. When they don't, @ditherkit/core is right there:
- Custom pipelines — you want to interleave dithering with other image operations
- Unsupported frameworks — Svelte, Vue, Solid, Qwik all work fine with the raw algorithms
- Build scripts —
pnpm build:imagesthat processes a folder of source files at CI time - Unusual runtimes — Cloudflare Workers, Deno, Bun edge functions, WebAssembly hosts
- Minimal bundles — if your bundle size matters down to the KB, skipping the wrapper lets you include only what you need
The algorithms are pure functions taking a pixel buffer. There's no hidden state, no React dependency, no DOM requirement. Drop them in anywhere.