| name | images |
| description | Understand and configure image optimization in deco.cx storefronts — components, quality settings, bypass rules, platform-specific handling, CDN proxy, and format negotiation. Use when the user asks about images, image quality, image optimization, or image components. |
Images in deco.cx
Deco provides a full image optimization pipeline: components that generate responsive markup, a multi-engine server-side optimizer, platform-aware URL rewriting, and CDN caching with next-gen format negotiation.
Image Components
Image
Import: apps/website/components/Image.tsx
The primary image component. Wraps a standard <img> with automatic srcset generation, optimization URL rewriting, and optional preloading.
| Prop | Type | Default | Description |
|---|
src | string | (required) | Image source URL |
width | number | (required) | Display width — used for aspect ratio and srcset |
height | number | — | Display height |
quality | QualityOptions | context default | "low" (60) | "medium" (70) | "high" (80) | "original" (100) |
fit | string | — | "contain" or "cover" |
preload | boolean | false | Adds <link rel="preload"> for LCP optimization |
fetchPriority | string | — | "high" | "low" | "auto" |
loading | string | "lazy" | Native lazy loading attribute |
import Image from "apps/website/components/Image.tsx";
<Image
src="https://example.com/hero.jpg"
width={1200}
height={600}
quality="high"
preload
fetchPriority="high"
loading="eager"
/>
Picture and Source
Import: apps/website/components/Picture.tsx
Use Picture with Source children for art-direction — serving different images or crops per breakpoint.
| Source Prop | Type | Description |
|---|
src | string | Image source URL |
width | number | Image width |
height | number | Image height |
quality | QualityOptions | Quality override |
media | string | CSS media query (e.g. "(max-width: 767px)") |
preload | boolean | Enable preload for this source |
import { Picture, Source } from "apps/website/components/Picture.tsx";
<Picture preload>
<Source src="/mobile.jpg" width={360} height={480} media="(max-width: 767px)" />
<Source src="/desktop.jpg" width={1200} height={600} media="(min-width: 768px)" />
<img src="/desktop.jpg" width={1200} height={600} loading="eager" />
</Picture>
Video
Import: apps/website/components/Video.tsx
Video element with optional image optimization via forceOptimizedSrc.
Responsive Images (srcset)
The Image and Source components automatically generate a srcset with density descriptors.
- Default factors:
[1, 2] — produces 1x and 2x variants
- When
preload is enabled, only the highest factor (2x) is used for the preload link to optimize LCP
- Custom srcset can be passed via the
srcSet prop to override automatic generation
Default Image Quality
Quality Levels
| Value | Encoded quality |
|---|
"low" | 60 |
"medium" | 70 |
"high" | 80 |
"original" | 100 |
Setting a Site-Wide Default
Configure defaultImageQuality in the website app props. This sets the quality for every Image, Source, and Picture component that doesn't explicitly override it.
- Type:
DefaultQualityOptions — allows "low", "medium", or "high" (excludes "original" to protect performance)
- Provided to components via
DefaultImageQualityContext (React context)
- Any component can override the default with its own
quality prop
This is exposed as a CMS panel field, not just code. In the CMS it appears under the site config (site settings → the website app) as "Default Image Quality" — "The default quality for images when not explicitly set per component" — a dropdown with low / medium / high. Anyone can change it from the UI without touching the repo.
⚠️ Check this first when the whole site's images look soft/low-quality. If it's set to low (60), every image that doesn't set its own quality prop is degraded site-wide — a silent, global quality regression that no per-component change will fix. It's easy to miss because nobody "changed the code."
Recommended: high for storefronts where product/hero images matter (jewelry, fashion, etc.). medium (70) is an acceptable perf/quality balance; low (60) is usually too aggressive for a product catalog. Set it explicitly rather than relying on whatever the panel happens to hold.
Two different "blurry" causes — don't confuse them:
- Soft across the whole site → likely the Default Image Quality panel set to
low (this section).
- Pixelated on one banner/section, worse on retina → the source image is smaller than the slot needs (see Diagnosing blurry / pixelated images below). Quality won't fix that; you need a bigger source.
Diagnosing blurry / pixelated images (undersized source)
The most common "distorted image" report is not stretching — it's a source image with fewer pixels than the slot needs on retina (DPR2) screens, so the pixels get upscaled and blur.
Tell-tale sign: sharp when the file is opened directly, pixelated on the site. (Opening shows the small native size; the site displays it larger.)
Mechanism
- The
Image/Source declares a base width — the 1× size the slot occupies on a normal screen.
- deco auto-generates a 2× (and 3×) variant in the
srcset (see Responsive Images above). On a DPR2 device, width={700} makes the browser fetch the 1400px variant. (DPR = Device Pixel Ratio; "retina" = DPR ≥ 2; most phones and Macs are DPR2–3.)
- If the uploaded source is smaller than that (e.g. 542px for a 1400px slot), the pixels are invented by upscaling ~2.6×. No parameter fixes this —
quality="original" adds no detail that was never photographed, only bytes.
Required source pixels = rendered CSS width × DPR. A card that renders ~700px wide needs 1400px on DPR2. Secondary effect: if the source aspect ratio ≠ the slot ratio, fit="cover" crops to fill (a slight crop people also read as "distorted").
Diagnose
1. Measure the real source (not the served size) — request it without width/height so the proxy returns the original, then measure:
curl -sL "https://decoims.com/image?quality=original&src=<ENCODED_SRC>" -o /tmp/orig.webp
sips -g pixelWidth -g pixelHeight /tmp/orig.webp
⚠️ If you leave width=1400 in the URL, naturalWidth reports 1400 — the already-upscaled output, not the source. The real size only shows with the resize params removed.
2. Find what the component asks for — read the Image/Source width. The retina requirement is width × 2:
<Source src={imageDesktop} width={700} height={342} media="(min-width: 1024px)" />
<Source src={imageMobile} width={375} height={184} media="(max-width: 1023px)" />
Verdict: source pixels ≥ width × 2 → sharp. Source < width × 2 → blurry (upscaled). That's the bug.
Fix
Upload a larger source. Minimum = the slot's 2× size, in the slot's aspect ratio (bigger is fine — the proxy downscales, which never blurs; smaller is the bug).
- Desktop card ~700px wide → 1400×684 (≈2.05:1)
- Full-width mobile ~375px wide → 750×368 (or 700×700 if the mobile
Source is square — see below)
Things that do not fix it: changing width/height (the on-screen slot is unchanged, the browser upscales anyway); quality="original" (no detail added — drop it to "high" to save bytes); CDN sharpening (deco/VTEX barely sharpens); shrinking the component in CSS (only helps if shrunk to ≤ source/2 CSS px, and it changes the design for every reuse of the component).
No larger original? Upscale with AI (e.g. Upscayl, free/local) — pick a scale that clears the target (3× on 542px → 1626px > 1400 ✓; 2× → 1084px < 1400 ✗). Caveat: AI invents detail — zoom in and verify fine features (gemstones, engraved text, logos) before shipping. A real higher-res photo always beats an upscale.
Keep the admin guidance honest
deco renders a field's @description / @title JSDoc as the CMS admin help text. If it states a 1× size (e.g. 572x280), editors upload undersized images. State the 2× retina target:
interface CardImage {
imageMobile: ImageWidget;
imageDesktop: ImageWidget;
}
Mobile vs desktop: watch the per-breakpoint Source
A Picture has a separate Source per media query, each with its own width/height — so the aspect ratio can differ between mobile and desktop. A common bug: desktop is a wide rectangle (700×342, ~2:1) but mobile is square (350×350, 1:1), making mobile fit="cover"-crop the photo into a square (subject floating in whitespace). Decide the intended mobile shape by looking at the live mobile render (emulate DPR and screenshot), not by guessing from code, then make the Source and the uploaded image agree (rectangular → 375×184, image 750×368; square → 350×350, image 700×700).
Plain-language explanation for a non-technical stakeholder
The image looks pixelated because the uploaded file is smaller than the space it fills on the site. The banner needs an image of 1400×684 px (and 750×368 px for mobile), but the current one is only 542×280 px, so the site has to stretch it. It looks sharp when you open the file directly because there it shows at its small original size; on the site it's enlarged. Fix: upload the image at the larger size — ideally the original high-resolution photo.
Image Optimization Pipeline
Two-Tier System
Deco uses a two-tier optimization strategy:
- Platform-specific optimization (default) — rewrites the URL using the e-commerce platform's native image CDN
- Deco optimization — falls back to the deco CDN proxy when no platform matches
Platform-Specific URL Rewriting
The component rewrites URLs based on the detected platform:
| Platform | URL strategy |
|---|
| VNDA | /{width}x{height}{pathname} |
| Shopify | Query params ?width=&height=&crop=center |
| VTEX | Path format {trueId}-{width}-{height} |
| Wake | Query params ?w=&h= |
| Sourei | Query params ?w=&h=&fit=&q= |
| Magento | Query params ?width=&height=&canvas=&optimize=low&fit= |
Deco CDN Proxy
When no platform matches, the image URL is rewritten to pass through the deco CDN:
https://decoims.com/image?src={src}&width={w}&height={h}&quality={q}&fit={f}
- Default host:
https://decoims.com
- Override:
DECO_CDN_HOST environment variable or window.DECO.featureFlags.cdnHost browser flag
Data URLs
Images with src starting with data: are returned as-is — no optimization is applied.
Image Engines (Server-Side)
The image loader (/live/invoke/website/loaders/image.ts) picks an engine based on the runtime environment.
| Engine | When selected | How it works |
|---|
| Pass-through | IMAGES_ENGINE=pass-through or default | Returns the original URL with no processing |
| WASM | Worker available (Deno) | Server-side encoding via WebAssembly — supports JPEG, PNG, WebP, AVIF |
| Cloudflare | Running in Cloudflare Workers | Uses cf.image transform: fetch(src, { cf: { image: { format, fit, width, height, quality } } }) |
| Deco (ImageKit) | Fallback | Proxies through ImageKit: https://ik.imagekit.io/{id}/tr:w-{w},h-{h},q-{q}/{src} |
Environment Variables
| Variable | Values | Description |
|---|
IMAGES_ENGINE | pass-through | (unset) | Force a specific engine |
DECO_IK_ID | string (default "decocx") | ImageKit account ID for the Deco engine |
Bypass Rules
Bypass Platform Image Optimization
Removed in v1.152.0. The BYPASS_PLATFORM_IMAGE_OPTIMIZATION env var and window.DECO.featureFlags.bypassPlatformImageOptimization browser flag were removed. Platform-specific URL rewriting is now always active when a platform is detected. If you are on a version older than 1.152.0, these flags still apply.
Bypass Deco Image Optimization
Disables all optimization — returns original image URLs untouched.
- Env var:
BYPASS_DECO_IMAGE_OPTIMIZATION=true
- Browser flag:
window.DECO.featureFlags.bypassDecoImageOptimization
Use for debugging or when images are pre-optimized externally.
Proxy Disable & Whitelist
Configured in the website app props (mod.ts):
disableProxy: true — disables the image proxy entirely
whitelistURLs — array of URL patterns; only matching sources are proxied (empty = allow all). Returns 403 for non-matching sources.
Format Negotiation
The image loader negotiates the best format based on the browser's Accept header:
- AVIF — served when
image/avif is in Accept (smallest file size, widest modern support)
- WebP — served when
image/webp is in Accept
- Original format — fallback when neither is supported
Format detection also uses binary signature matching (byte-level) with fallback to Content-Type header.
Supported formats for processing: JPEG, PNG, WebP, AVIF.
Caching
Optimized images are cached aggressively:
Cache-Control: public, s-maxage=15552000, max-age=15552000, immutable
This is ~6 months of immutable caching. The loader also uses the Cache API server-side to avoid re-processing identical requests.
Response headers include:
x-img-engine — which engine processed the image
x-cache — HIT or MISS for the server-side cache
Performance Best Practices
- Preload LCP images — set
preload and fetchPriority="high" on the hero/banner image
- Set width and height — prevents Cumulative Layout Shift (CLS)
- Use
Picture for responsive art direction — serve appropriately sized images per breakpoint
- Use
"high" quality for hero images, "medium" or "low" for thumbnails — balance visual quality vs file size
- Set a site-wide
defaultImageQuality explicitly — avoid forgetting quality on individual components. Prefer "high" for product/hero-heavy storefronts ("medium" is an acceptable balance). Verify the CMS "Default Image Quality" panel isn't silently on low — it degrades every non-overriding image site-wide
- Let lazy loading work — only set
loading="eager" on above-the-fold images
- Don't bypass optimization without reason — the platform and deco CDN pipelines significantly reduce payload size
Asset URL Prefixes
Before sending images to the CDN proxy, these known asset prefixes are stripped to hit storage directly:
https://decoims.com/
https://storage.googleapis.com/deco-assets/
https://assets.decocache.com/
https://deco-sites-assets.s3.sa-east-1.amazonaws.com/
https://data.decoassets.com/
Quick Reference — All Environment Variables & Feature Flags
| Variable / Flag | Type | Description |
|---|
BYPASS_PLATFORM_IMAGE_OPTIMIZATION | env | Removed in v1.152.0. Previously skipped platform CDN to use deco proxy |
BYPASS_DECO_IMAGE_OPTIMIZATION | env | Skip all optimization |
DECO_CDN_HOST | env | Override CDN host (default https://decoims.com) |
IMAGES_ENGINE | env | Force image engine (pass-through) |
DECO_IK_ID | env | ImageKit account ID (default decocx) |
window.DECO.featureFlags.bypassPlatformImageOptimization | browser | Removed in v1.152.0. Previously same as env, client-side |
window.DECO.featureFlags.bypassDecoImageOptimization | browser | Same as env, client-side |
window.DECO.featureFlags.cdnHost | browser | Override CDN host, client-side |