Preserving P3 Color Space in Next.js Image Optimization
Next.js Image optimization destroys HDR colors
Next.js Image handles resizing, lazy loading, format conversion, and caching automatically. It also destroys wide color gamut images by converting everything to sRGB. If you work with color-critical content, you get fast images that look washed out.
I built a workaround that preserves P3 color space while keeping most of Next.js Image’s optimization benefits, and proposed a native fix upstream.
When optimization destroys beauty
Under the hood, Next.js Image uses Sharp.js for server-side processing. Sharp strips ICC color profiles by default and converts to sRGB. Good for compatibility. Bad for anything designed in Display P3.
The pipeline takes your P3 PNGs, runs them through Sharp, and outputs sRGB WebP. Great compression, washed out colors.
Images designed in Display P3 contain colors that don’t exist in sRGB. Sharp clips them to the nearest sRGB equivalent, and you end up with noticeably duller results on P3-capable displays.
The fix is two lines of Sharp config:
Today, the only escape hatch is the unoptimized prop, which bypasses Sharp entirely:
This preserves your colors but kills every optimization benefit: no resizing, no format conversion, no responsive srcset. Fine for a hero image or two, but it doesn’t scale.
Understanding P3 Color Space
While sRGB has served as the web standard since the 1990s, it covers only a fraction of colors the human eye can perceive.
Display P3 is a superset of sRGB, meaning any sRGB color can be represented in P3, but P3 can represent approximately 25% wider gamutDean Jackson, “Improving Color on the Web,” WebKit Blog, June 2016 than sRGB, particularly in the deep reds and vibrant greens that make interface designs pop. A color like #ff0000 in sRGB becomes a much more saturated red when interpreted as P3, while colors that exist in P3 space simply cannot be represented in sRGB at all.
The technical mechanism is straightforward: P3 uses wider primary color coordinates than sRGB. When a color exists in P3 but not in sRGB, conversion clips it to the nearest representable value.
The practical impact is substantial. Modern displays like MacBooks, iMacs, high-end monitors, and most mobile devices can reproduce P3 colors. Safari has supported P3 in CSS since 2016, and Chrome finally achieved full support in 2023“Chrome 111 Beta: CSS Color Level 4,” Chrome for Developers, February 2023. We’re at an inflection point where wide color gamut is becoming the norm, not the exception.
For UI design, this expanded palette is particularly valuable. Interface elements that rely on color intensity for hierarchy, vibrant accent colors that guide user attention, and the subtle gradients that give modern designs their depth all benefit dramatically from P3’s expanded range.
A color-preserving P3Image component
Rather than fighting Next.js Image’s architecture, I built a P3Image component that wraps it. Local images get routed through a custom API that processes them with Sharp’s ICC preservation. External URLs pass through unchanged.
The component is a client-side URL generator. It builds requests that trigger server-side Sharp processing while keeping all of Next.js Image’s client behavior: lazy loading, responsive srcset, event handlers.
The loader routes local images through our API and passes external URLs through unchanged:
On the server side, the processor checks for existing ICC profiles and either preserves them with keepIccProfile() or adds a P3 profile with withIccProfile('p3') for images that lack color information. See Sharp’s ICC profile API for details.
The smartSubsample: false setting prevents chroma subsampling that degrades color accuracy. For JPEG output, chromaSubsampling: '4:4:4' replaces the default '4:2:0' to maintain full color resolution. These settings add 10-20% processing time per image, but the results are cached.
My implementation also includes intelligent caching and security measures:
The caching strategy compares modification times between source and cached files, invalidating when sources change. Path traversal is blocked at the API route level.
Performance tradeoffs
The main cost is losing Vercel’s edge network for image processing. Our API runs on the server or serverless function, so first loads add 50-250ms depending on geographic distance. Cached loads are identical to native Next.js.
Everything else carries over: lazy loading, responsive srcset, preload={true} for above-fold images, onLoad and onError handlers. We also get P3-aware WebP, JPEG, and PNG output, plus a custom X-P3-Preserved header for debugging.
What we lose: edge distribution (solvable with a CDN), automatic AVIF support (could be added), and auto format detection (we explicitly set output format). For most sites, cached requests are the majority of traffic, so the practical impact is small.
unoptimized vs. preserveColorProfile
The file size difference tells the story. Using unoptimized bypasses Sharp entirely, serving the original file at every viewport width. With preserveColorProfile, Sharp still resizes, compresses, and generates responsive srcset variants while keeping the ICC profile intact.
Measured from a 2250×1500 lossless P3 WebP source, processed with Sharp at quality 75. Default <Image> strips the ICC profile and converts to sRGB. preserveColorProfile retains full Display P3 fidelity at nearly identical file sizes.
You can also apply P3 selectively:
Testing with P3-gamut design interfaces showed the difference clearly. On P3-capable displays, the blues, purples, and greens in UI mockups were visibly more saturated and true to the original design files. First loads ran 100-300ms versus 50-200ms for standard Next.js, with cached loads identical at 10-50ms and only a 2KB bundle size increase.
When to use P3Image
Use P3Image for color-critical content: UI design screenshots, photography, product images for premium brands, portfolio pieces, brand materials. Use standard Next.js Image for everything else: icons, diagrams, thumbnails where loading speed matters more than color accuracy, monochrome or low-saturation content.
A case for native support
The P3Image component works, but it shouldn’t be necessary. The fix is two lines in Sharp’s pipeline. There’s no reason this can’t live in Next.js itself.
I’ve opened a draft PR on the Next.js repository proposing a preserveColorProfile option at both the global config and per-image level:
When enabled, the optimizer calls keepIccProfile(), disables smartSubsample for WebP, and uses chromaSubsampling: '4:4:4' for JPEG. It defaults to false, so existing behavior is untouched.
This would eliminate the need for custom components, API routes, and caching layers. The global config handles sites where everything should preserve color. The per-image prop handles selective cases. Either way, no extra infrastructure.
P3-capable displays are the norm now. Silently converting everything to sRGB is an increasingly bad default.

